Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c35a8741a | |||
| 329e795648 |
456
admin.py
456
admin.py
@@ -9,7 +9,8 @@ from sqlalchemy import func, desc
|
||||
import json
|
||||
|
||||
from models import (db, User, Translation, TranslationCache, GuestTranslation,
|
||||
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig)
|
||||
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig,
|
||||
UserTypeConfig, MembershipPlanConfig)
|
||||
from config import USER_LIMITS, MEMBERSHIP_PLANS
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
@@ -329,11 +330,15 @@ def settings():
|
||||
# 获取动态配置
|
||||
dynamic_configs = DynamicConfig.query.all()
|
||||
|
||||
# 获取LLM动态配置
|
||||
llm_config = get_llm_config()
|
||||
|
||||
return render_template('admin/settings.html',
|
||||
configs=config_dict,
|
||||
dynamic_configs=dynamic_configs,
|
||||
user_limits=USER_LIMITS,
|
||||
membership_plans=MEMBERSHIP_PLANS
|
||||
membership_plans=MEMBERSHIP_PLANS,
|
||||
llm_config=llm_config
|
||||
)
|
||||
|
||||
|
||||
@@ -767,4 +772,449 @@ def get_llm_config():
|
||||
'max_tokens': DynamicConfig.get('llm_max_tokens', LLM_CONFIG.get('max_tokens')),
|
||||
'chunk_size': DynamicConfig.get('llm_chunk_size', LLM_CONFIG.get('chunk_size')),
|
||||
'timeout': DynamicConfig.get('llm_timeout', LLM_CONFIG.get('timeout')),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ==================== 用户类型配置管理(动态增删) ====================
|
||||
@admin_bp.route('/user-types')
|
||||
@admin_required
|
||||
def user_types():
|
||||
"""用户类型配置列表"""
|
||||
user_types = UserTypeConfig.query.order_by(UserTypeConfig.sort_order).all()
|
||||
|
||||
# 如果数据库中没有数据,初始化默认配置
|
||||
if not user_types:
|
||||
init_default_user_types()
|
||||
user_types = UserTypeConfig.query.order_by(UserTypeConfig.sort_order).all()
|
||||
|
||||
return render_template('admin/user_types.html', user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/add', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def add_user_type():
|
||||
"""添加用户类型"""
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
# 检查type_key是否已存在
|
||||
existing = UserTypeConfig.query.filter_by(type_key=data.get('type_key')).first()
|
||||
if existing:
|
||||
return jsonify({'error': '类型标识已存在'}), 400
|
||||
|
||||
user_type = UserTypeConfig(
|
||||
type_key=data.get('type_key'),
|
||||
display_name=data.get('display_name'),
|
||||
daily_translations=int(data.get('daily_translations', 10)),
|
||||
max_pages=int(data.get('max_pages', 50)),
|
||||
max_file_size=int(data.get('max_file_size_mb', 30)) * 1024 * 1024,
|
||||
features=data.get('features', '[]'),
|
||||
sort_order=int(data.get('sort_order', 0)),
|
||||
is_active=data.get('is_active', True),
|
||||
is_system=False,
|
||||
)
|
||||
|
||||
db.session.add(user_type)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='add_user_type',
|
||||
target=user_type.type_key,
|
||||
detail=json.dumps(user_type.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'user_type': user_type.to_dict()})
|
||||
flash('用户类型已添加', 'success')
|
||||
return redirect(url_for('admin.user_types'))
|
||||
|
||||
# 所有可用功能
|
||||
all_features = [
|
||||
{'key': 'basic_translate', 'name': '基础翻译'},
|
||||
{'key': 'compare_view', 'name': '对照查看'},
|
||||
{'key': 'retranslate', 'name': '重新翻译'},
|
||||
{'key': 'history', 'name': '历史记录'},
|
||||
{'key': 'priority_queue', 'name': '优先队列'},
|
||||
{'key': 'export_pdf', 'name': '导出PDF'},
|
||||
{'key': 'batch_translate', 'name': '批量翻译'},
|
||||
{'key': 'custom_terms', 'name': '自定义术语'},
|
||||
]
|
||||
|
||||
return render_template('admin/user_type_form.html', user_type=None, all_features=all_features)
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/<int:type_id>/edit', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def edit_user_type(type_id):
|
||||
"""编辑用户类型"""
|
||||
user_type = UserTypeConfig.query.get_or_404(type_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
user_type.display_name = data.get('display_name', user_type.display_name)
|
||||
user_type.daily_translations = int(data.get('daily_translations', user_type.daily_translations))
|
||||
user_type.max_pages = int(data.get('max_pages', user_type.max_pages))
|
||||
user_type.max_file_size = int(data.get('max_file_size_mb', user_type.max_file_size // 1024 // 1024)) * 1024 * 1024
|
||||
user_type.features = data.get('features', user_type.features)
|
||||
user_type.sort_order = int(data.get('sort_order', user_type.sort_order))
|
||||
user_type.is_active = data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='edit_user_type',
|
||||
target=user_type.type_key,
|
||||
detail=json.dumps(user_type.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'user_type': user_type.to_dict()})
|
||||
flash('用户类型已更新', 'success')
|
||||
return redirect(url_for('admin.user_types'))
|
||||
|
||||
# 所有可用功能
|
||||
all_features = [
|
||||
{'key': 'basic_translate', 'name': '基础翻译'},
|
||||
{'key': 'compare_view', 'name': '对照查看'},
|
||||
{'key': 'retranslate', 'name': '重新翻译'},
|
||||
{'key': 'history', 'name': '历史记录'},
|
||||
{'key': 'priority_queue', 'name': '优先队列'},
|
||||
{'key': 'export_pdf', 'name': '导出PDF'},
|
||||
{'key': 'batch_translate', 'name': '批量翻译'},
|
||||
{'key': 'custom_terms', 'name': '自定义术语'},
|
||||
]
|
||||
|
||||
return render_template('admin/user_type_form.html', user_type=user_type, all_features=all_features)
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/<int:type_id>/delete', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_user_type(type_id):
|
||||
"""删除用户类型"""
|
||||
user_type = UserTypeConfig.query.get_or_404(type_id)
|
||||
|
||||
# 系统内置类型不可删除
|
||||
if user_type.is_system:
|
||||
return jsonify({'error': '系统内置类型不可删除'}), 400
|
||||
|
||||
# 检查是否有用户使用此类型
|
||||
users_count = User.query.filter_by(user_type=user_type.type_key).count()
|
||||
if users_count > 0:
|
||||
return jsonify({'error': f'有 {users_count} 个用户使用此类型,请先修改用户类型'}), 400
|
||||
|
||||
type_key = user_type.type_key
|
||||
db.session.delete(user_type)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='delete_user_type',
|
||||
target=type_key
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/<int:type_id>/toggle', methods=['POST'])
|
||||
@admin_required
|
||||
def toggle_user_type(type_id):
|
||||
"""切换用户类型状态"""
|
||||
user_type = UserTypeConfig.query.get_or_404(type_id)
|
||||
user_type.is_active = not user_type.is_active
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'is_active': user_type.is_active})
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/init', methods=['POST'])
|
||||
@admin_required
|
||||
def init_user_types():
|
||||
"""初始化默认用户类型"""
|
||||
init_default_user_types()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
def init_default_user_types():
|
||||
"""初始化默认用户类型配置"""
|
||||
from config import USER_LIMITS
|
||||
|
||||
defaults = [
|
||||
('guest', '访客', USER_LIMITS.get('guest', {}), 0, True),
|
||||
('free', '免费用户', USER_LIMITS.get('free', {}), 1, True),
|
||||
('vip_basic', '基础会员', USER_LIMITS.get('vip_basic', {}), 2, True),
|
||||
('vip_pro', '专业会员', USER_LIMITS.get('vip_pro', {}), 3, True),
|
||||
('vip_enterprise', '企业会员', USER_LIMITS.get('vip_enterprise', {}), 4, True),
|
||||
]
|
||||
|
||||
for type_key, display_name, limits, sort_order, is_system in defaults:
|
||||
existing = UserTypeConfig.query.filter_by(type_key=type_key).first()
|
||||
if not existing:
|
||||
user_type = UserTypeConfig(
|
||||
type_key=type_key,
|
||||
display_name=display_name,
|
||||
daily_translations=limits.get('daily_translations', 10),
|
||||
max_pages=limits.get('max_pages', 50),
|
||||
max_file_size=limits.get('max_file_size', 30*1024*1024),
|
||||
features=json.dumps(limits.get('features', []), ensure_ascii=False),
|
||||
sort_order=sort_order,
|
||||
is_active=True,
|
||||
is_system=is_system,
|
||||
)
|
||||
db.session.add(user_type)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# ==================== 会员套餐配置管理(动态增删) ====================
|
||||
@admin_bp.route('/membership-plans')
|
||||
@admin_required
|
||||
def membership_plans():
|
||||
"""会员套餐配置列表"""
|
||||
plans = MembershipPlanConfig.query.order_by(MembershipPlanConfig.sort_order).all()
|
||||
|
||||
# 如果数据库中没有数据,初始化默认配置
|
||||
if not plans:
|
||||
init_default_membership_plans()
|
||||
plans = MembershipPlanConfig.query.order_by(MembershipPlanConfig.sort_order).all()
|
||||
|
||||
# 获取所有用户类型供选择
|
||||
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
|
||||
|
||||
return render_template('admin/membership_plans.html', plans=plans, user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/add', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def add_membership_plan():
|
||||
"""添加会员套餐"""
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
# 检查plan_key是否已存在
|
||||
existing = MembershipPlanConfig.query.filter_by(plan_key=data.get('plan_key')).first()
|
||||
if existing:
|
||||
return jsonify({'error': '套餐标识已存在'}), 400
|
||||
|
||||
plan = MembershipPlanConfig(
|
||||
plan_key=data.get('plan_key'),
|
||||
display_name=data.get('display_name'),
|
||||
price=float(data.get('price', 0)),
|
||||
original_price=float(data.get('original_price')) if data.get('original_price') else None,
|
||||
period=data.get('period', 'month'),
|
||||
period_days=int(data.get('period_days', 30)),
|
||||
description=data.get('description'),
|
||||
user_type_key=data.get('user_type_key'),
|
||||
sort_order=int(data.get('sort_order', 0)),
|
||||
is_active=data.get('is_active', True),
|
||||
is_recommended=data.get('is_recommended', False),
|
||||
is_system=False,
|
||||
)
|
||||
|
||||
db.session.add(plan)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='add_membership_plan',
|
||||
target=plan.plan_key,
|
||||
detail=json.dumps(plan.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'plan': plan.to_dict()})
|
||||
flash('会员套餐已添加', 'success')
|
||||
return redirect(url_for('admin.membership_plans'))
|
||||
|
||||
# 获取所有用户类型供选择
|
||||
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
|
||||
|
||||
return render_template('admin/membership_plan_form.html', plan=None, user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/edit', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def edit_membership_plan(plan_id):
|
||||
"""编辑会员套餐"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
plan.display_name = data.get('display_name', plan.display_name)
|
||||
plan.price = float(data.get('price', plan.price))
|
||||
plan.original_price = float(data.get('original_price')) if data.get('original_price') else None
|
||||
plan.period = data.get('period', plan.period)
|
||||
plan.period_days = int(data.get('period_days', plan.period_days))
|
||||
plan.description = data.get('description', plan.description)
|
||||
plan.user_type_key = data.get('user_type_key', plan.user_type_key)
|
||||
plan.sort_order = int(data.get('sort_order', plan.sort_order))
|
||||
plan.is_active = data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true'
|
||||
plan.is_recommended = data.get('is_recommended', False) if isinstance(data.get('is_recommended'), bool) else data.get('is_recommended') == 'true'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='edit_membership_plan',
|
||||
target=plan.plan_key,
|
||||
detail=json.dumps(plan.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'plan': plan.to_dict()})
|
||||
flash('会员套餐已更新', 'success')
|
||||
return redirect(url_for('admin.membership_plans'))
|
||||
|
||||
# 获取所有用户类型供选择
|
||||
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
|
||||
|
||||
return render_template('admin/membership_plan_form.html', plan=plan, user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/delete', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_membership_plan(plan_id):
|
||||
"""删除会员套餐"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
|
||||
# 系统内置套餐不可删除
|
||||
if plan.is_system:
|
||||
return jsonify({'error': '系统内置套餐不可删除'}), 400
|
||||
|
||||
plan_key = plan.plan_key
|
||||
db.session.delete(plan)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='delete_membership_plan',
|
||||
target=plan_key
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/toggle', methods=['POST'])
|
||||
@admin_required
|
||||
def toggle_membership_plan(plan_id):
|
||||
"""切换会员套餐状态"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
plan.is_active = not plan.is_active
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'is_active': plan.is_active})
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/recommend', methods=['POST'])
|
||||
@admin_required
|
||||
def recommend_membership_plan(plan_id):
|
||||
"""设置推荐套餐"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
plan.is_recommended = not plan.is_recommended
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'is_recommended': plan.is_recommended})
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/init', methods=['POST'])
|
||||
@admin_required
|
||||
def init_membership_plans():
|
||||
"""初始化默认会员套餐"""
|
||||
init_default_membership_plans()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
def init_default_membership_plans():
|
||||
"""初始化默认会员套餐配置"""
|
||||
from config import MEMBERSHIP_PLANS
|
||||
|
||||
defaults = [
|
||||
('vip_basic', MEMBERSHIP_PLANS.get('vip_basic', {}), 'vip_basic', 0, True),
|
||||
('vip_pro', MEMBERSHIP_PLANS.get('vip_pro', {}), 'vip_pro', 1, True),
|
||||
('vip_enterprise', MEMBERSHIP_PLANS.get('vip_enterprise', {}), 'vip_enterprise', 2, True),
|
||||
]
|
||||
|
||||
for plan_key, plan_data, user_type_key, sort_order, is_system in defaults:
|
||||
existing = MembershipPlanConfig.query.filter_by(plan_key=plan_key).first()
|
||||
if not existing:
|
||||
plan = MembershipPlanConfig(
|
||||
plan_key=plan_key,
|
||||
display_name=plan_data.get('name', plan_key),
|
||||
price=plan_data.get('price', 0),
|
||||
original_price=None,
|
||||
period=plan_data.get('period', 'month'),
|
||||
period_days=30 if plan_data.get('period') == 'month' else 365 if plan_data.get('period') == 'year' else 90,
|
||||
description=plan_data.get('description', ''),
|
||||
user_type_key=user_type_key,
|
||||
sort_order=sort_order,
|
||||
is_active=True,
|
||||
is_recommended=False,
|
||||
is_system=is_system,
|
||||
)
|
||||
db.session.add(plan)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# ==================== API: 获取用户限制配置(供其他模块使用) ====================
|
||||
def get_user_limits(user_type_key):
|
||||
"""获取指定用户类型的限制配置"""
|
||||
config = UserTypeConfig.query.filter_by(type_key=user_type_key, is_active=True).first()
|
||||
if config:
|
||||
return {
|
||||
'daily_translations': config.daily_translations,
|
||||
'max_pages': config.max_pages,
|
||||
'max_file_size': config.max_file_size,
|
||||
'features': config.get_features(),
|
||||
}
|
||||
# 如果数据库中没有,使用默认配置
|
||||
from config import USER_LIMITS
|
||||
return USER_LIMITS.get(user_type_key, USER_LIMITS.get('free', {}))
|
||||
|
||||
|
||||
def get_all_user_types():
|
||||
"""获取所有用户类型配置"""
|
||||
types = UserTypeConfig.query.filter_by(is_active=True).order_by(UserTypeConfig.sort_order).all()
|
||||
return {t.type_key: get_user_limits(t.type_key) for t in types}
|
||||
|
||||
|
||||
def get_membership_plan(plan_key):
|
||||
"""获取指定会员套餐配置"""
|
||||
plan = MembershipPlanConfig.query.filter_by(plan_key=plan_key, is_active=True).first()
|
||||
if plan:
|
||||
return plan.to_dict()
|
||||
from config import MEMBERSHIP_PLANS
|
||||
return MEMBERSHIP_PLANS.get(plan_key, {})
|
||||
|
||||
|
||||
def get_all_membership_plans():
|
||||
"""获取所有会员套餐配置"""
|
||||
plans = MembershipPlanConfig.query.filter_by(is_active=True).order_by(MembershipPlanConfig.sort_order).all()
|
||||
return [p.to_dict() for p in plans]
|
||||
105
models.py
105
models.py
@@ -417,4 +417,107 @@ class DynamicConfig(db.Model):
|
||||
db.session.add(config)
|
||||
|
||||
db.session.commit()
|
||||
return config
|
||||
return config
|
||||
|
||||
|
||||
# ==================== 用户类型配置(动态增删) ====================
|
||||
class UserTypeConfig(db.Model):
|
||||
"""用户类型配置表 - 支持动态增删"""
|
||||
__tablename__ = 'user_type_config'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type_key = db.Column(db.String(50), unique=True, nullable=False) # 类型标识: guest, free, vip_basic, etc.
|
||||
display_name = db.Column(db.String(100), nullable=False) # 显示名称: 访客, 免费用户, 基础会员
|
||||
|
||||
# 权限配置
|
||||
daily_translations = db.Column(db.Integer, default=10) # 每日翻译次数 (-1=无限)
|
||||
max_pages = db.Column(db.Integer, default=50) # 最大页数 (-1=无限)
|
||||
max_file_size = db.Column(db.Integer, default=30*1024*1024) # 最大文件大小(bytes)
|
||||
|
||||
# 功能列表(JSON)
|
||||
features = db.Column(db.Text, default='[]') # JSON数组: ["basic_translate", "history"]
|
||||
|
||||
# 排序和状态
|
||||
sort_order = db.Column(db.Integer, default=0) # 排序权重
|
||||
is_active = db.Column(db.Boolean, default=True) # 是否启用
|
||||
is_system = db.Column(db.Boolean, default=False) # 是否系统内置(不可删除)
|
||||
|
||||
# 时间
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def get_features(self):
|
||||
"""获取功能列表"""
|
||||
import json
|
||||
try:
|
||||
return json.loads(self.features) if self.features else []
|
||||
except:
|
||||
return []
|
||||
|
||||
def set_features(self, feature_list):
|
||||
"""设置功能列表"""
|
||||
import json
|
||||
self.features = json.dumps(feature_list, ensure_ascii=False)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'type_key': self.type_key,
|
||||
'display_name': self.display_name,
|
||||
'daily_translations': self.daily_translations,
|
||||
'max_pages': self.max_pages,
|
||||
'max_file_size': self.max_file_size,
|
||||
'features': self.get_features(),
|
||||
'sort_order': self.sort_order,
|
||||
'is_active': self.is_active,
|
||||
'is_system': self.is_system,
|
||||
}
|
||||
|
||||
|
||||
# ==================== 会员套餐配置(动态增删) ====================
|
||||
class MembershipPlanConfig(db.Model):
|
||||
"""会员套餐配置表 - 支持动态增删"""
|
||||
__tablename__ = 'membership_plan_config'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
plan_key = db.Column(db.String(50), unique=True, nullable=False) # 套餐标识: vip_basic, vip_pro, etc.
|
||||
display_name = db.Column(db.String(100), nullable=False) # 显示名称: 基础会员
|
||||
|
||||
# 价格配置
|
||||
price = db.Column(db.Float, default=0) # 价格
|
||||
original_price = db.Column(db.Float, nullable=True) # 原价(用于折扣显示)
|
||||
period = db.Column(db.String(20), default='month') # 周期: month, quarter, year
|
||||
period_days = db.Column(db.Integer, default=30) # 周期天数
|
||||
|
||||
# 描述
|
||||
description = db.Column(db.String(255), nullable=True) # 套餐描述
|
||||
|
||||
# 对应的用户类型
|
||||
user_type_key = db.Column(db.String(50), nullable=True) # 购买后升级到的用户类型
|
||||
|
||||
# 排序和状态
|
||||
sort_order = db.Column(db.Integer, default=0)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
is_recommended = db.Column(db.Boolean, default=False) # 是否推荐
|
||||
is_system = db.Column(db.Boolean, default=False) # 是否系统内置(不可删除)
|
||||
|
||||
# 时间
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'plan_key': self.plan_key,
|
||||
'display_name': self.display_name,
|
||||
'price': self.price,
|
||||
'original_price': self.original_price,
|
||||
'period': self.period,
|
||||
'period_days': self.period_days,
|
||||
'description': self.description,
|
||||
'user_type_key': self.user_type_key,
|
||||
'sort_order': self.sort_order,
|
||||
'is_active': self.is_active,
|
||||
'is_recommended': self.is_recommended,
|
||||
'is_system': self.is_system,
|
||||
}
|
||||
240
templates/admin/membership_plan_form.html
Normal file
240
templates/admin/membership_plan_form.html
Normal file
@@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% if plan %}编辑会员套餐{% else %}添加会员套餐{% endif %} - 后台管理</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #f5f5f5; }
|
||||
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
|
||||
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.main-content { margin-left: 250px; padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
|
||||
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
|
||||
</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.packages') }}"><i class="bi bi-box-seam"></i> 数据包套餐</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
|
||||
</ul>
|
||||
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
|
||||
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4>
|
||||
<i class="bi bi-credit-card"></i>
|
||||
{% if plan %}编辑会员套餐{% else %}添加会员套餐{% endif %}
|
||||
</h4>
|
||||
<a href="{{ url_for('admin.membership_plans') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> 返回列表
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="planForm">
|
||||
{% if plan %}
|
||||
<input type="hidden" name="plan_id" value="{{ plan.id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">套餐标识 <small class="text-muted">(唯一标识符)</small></label>
|
||||
{% if plan %}
|
||||
<input type="text" class="form-control" name="plan_key"
|
||||
value="{{ plan.plan_key }}" readonly>
|
||||
<small class="text-muted">套餐标识创建后不可修改</small>
|
||||
{% else %}
|
||||
<input type="text" class="form-control" name="plan_key"
|
||||
placeholder="例如: vip_gold" required>
|
||||
<small class="text-muted">建议使用英文,如 vip_gold, trial_30days</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">显示名称</label>
|
||||
<input type="text" class="form-control" name="display_name"
|
||||
value="{{ plan.display_name if plan else '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">套餐描述</label>
|
||||
<textarea class="form-control" name="description" rows="2"
|
||||
placeholder="简短描述套餐特点">{{ plan.description if plan else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">购买后升级的用户类型</label>
|
||||
<select class="form-select" name="user_type_key">
|
||||
<option value="">-- 不升级用户类型 --</option>
|
||||
{% for ut in user_types %}
|
||||
<option value="{{ ut.type_key }}"
|
||||
{% if plan and plan.user_type_key == ut.type_key %}selected{% endif %}>
|
||||
{{ ut.display_name }} ({{ ut.type_key }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="text-muted">用户购买此套餐后将升级到指定类型</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">价格 (元)</label>
|
||||
<input type="number" class="form-control" name="price"
|
||||
step="0.01" value="{{ plan.price if plan else 0 }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">原价 (元) <small class="text-muted">(可选)</small></label>
|
||||
<input type="number" class="form-control" name="original_price"
|
||||
step="0.01" value="{{ plan.original_price if plan else '' }}"
|
||||
placeholder="用于显示折扣">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">计费周期</label>
|
||||
<select class="form-select" name="period" id="periodSelect">
|
||||
<option value="month" {% if plan and plan.period == 'month' %}selected{% endif %}>月</option>
|
||||
<option value="quarter" {% if plan and plan.period == 'quarter' %}selected{% endif %}>季</option>
|
||||
<option value="year" {% if plan and plan.period == 'year' %}selected{% endif %}>年</option>
|
||||
<option value="custom" {% if plan and plan.period not in ['month', 'quarter', 'year'] %}selected{% endif %}>自定义天数</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3" id="periodDaysGroup"
|
||||
style="display: {% if plan and plan.period not in ['month', 'quarter', 'year'] %}block{% else %}none{% endif %}">
|
||||
<label class="form-label">周期天数</label>
|
||||
<input type="number" class="form-control" name="period_days"
|
||||
value="{{ plan.period_days if plan else 30 }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">排序权重</label>
|
||||
<input type="number" class="form-control" name="sort_order"
|
||||
value="{{ plan.sort_order if plan else 0 }}">
|
||||
<small class="text-muted">数字越小排在前面</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_active"
|
||||
id="is_active" {% if plan and plan.is_active %}checked{% elif not plan %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_active">上架此套餐</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_recommended"
|
||||
id="is_recommended" {% if plan and plan.is_recommended %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_recommended">
|
||||
<i class="bi bi-star-fill text-warning"></i> 推荐此套餐
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> {% if plan %}保存修改{% else %}添加套餐{% endif %}
|
||||
</button>
|
||||
<a href="{{ url_for('admin.membership_plans') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle"></i> 取消
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// 周期选择联动
|
||||
document.getElementById('periodSelect').addEventListener('change', function() {
|
||||
const customGroup = document.getElementById('periodDaysGroup');
|
||||
if (this.value === 'custom') {
|
||||
customGroup.style.display = 'block';
|
||||
} else {
|
||||
customGroup.style.display = 'none';
|
||||
// 自动设置天数
|
||||
const daysInput = document.querySelector('input[name="period_days"]');
|
||||
if (this.value === 'month') daysInput.value = 30;
|
||||
else if (this.value === 'quarter') daysInput.value = 90;
|
||||
else if (this.value === 'year') daysInput.value = 365;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('planForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
const data = {
|
||||
plan_key: formData.get('plan_key'),
|
||||
display_name: formData.get('display_name'),
|
||||
description: formData.get('description'),
|
||||
user_type_key: formData.get('user_type_key'),
|
||||
price: parseFloat(formData.get('price')),
|
||||
original_price: formData.get('original_price') ? parseFloat(formData.get('original_price')) : null,
|
||||
period: formData.get('period'),
|
||||
period_days: parseInt(formData.get('period_days')),
|
||||
sort_order: parseInt(formData.get('sort_order')),
|
||||
is_active: formData.get('is_active') === 'on',
|
||||
is_recommended: formData.get('is_recommended') === 'on'
|
||||
};
|
||||
|
||||
{% if plan %}
|
||||
const url = `/admin/membership-plans/{{ plan.id }}/edit`;
|
||||
{% else %}
|
||||
const url = '/admin/membership-plans/add';
|
||||
{% endif %}
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
{% if plan %}
|
||||
alert('会员套餐已更新');
|
||||
{% else %}
|
||||
alert('会员套餐已添加');
|
||||
{% endif %}
|
||||
window.location.href = '/admin/membership-plans';
|
||||
} else {
|
||||
alert('操作失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
221
templates/admin/membership_plans.html
Normal file
221
templates/admin/membership_plans.html
Normal file
@@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>会员套餐配置 - 后台管理</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #f5f5f5; }
|
||||
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
|
||||
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.main-content { margin-left: 250px; padding: 20px; }
|
||||
.plan-card { border-radius: 12px; transition: all 0.3s; }
|
||||
.plan-card:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0,0,0,0.15); }
|
||||
.plan-card.basic { border-top: 4px solid #6c757d; }
|
||||
.plan-card.pro { border-top: 4px solid #ffc107; }
|
||||
.plan-card.enterprise { border-top: 4px solid #0d6efd; }
|
||||
.plan-card.custom { border-top: 4px solid #17a2b8; }
|
||||
.price-display { font-size: 2rem; font-weight: bold; }
|
||||
.badge-recommended { background: linear-gradient(135deg, #ffc107, #ff8c00); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
|
||||
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
|
||||
</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.packages') }}"><i class="bi bi-box-seam"></i> 数据包套餐</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
|
||||
</ul>
|
||||
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
|
||||
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4><i class="bi bi-credit-card"></i> 会员套餐配置</h4>
|
||||
<div>
|
||||
<a href="{{ url_for('admin.add_membership_plan') }}" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> 添加套餐
|
||||
</a>
|
||||
<a href="{{ url_for('admin.user_types') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-person-badge"></i> 用户类型配置
|
||||
</a>
|
||||
<a href="{{ url_for('admin.settings') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-gear"></i> 基础设置
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% for plan in plans %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card plan-card {% if plan.plan_key == 'vip_basic' %}basic{% elif plan.plan_key == 'vip_pro' %}pro{% elif plan.plan_key == 'vip_enterprise' %}enterprise{% else %}custom{% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
{% if plan.is_recommended %}
|
||||
<span class="badge badge-recommended me-2"><i class="bi bi-star-fill"></i> 推荐</span>
|
||||
{% endif %}
|
||||
{% if plan.is_system %}
|
||||
<span class="badge bg-danger me-2">系统</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info me-2">自定义</span>
|
||||
{% endif %}
|
||||
{{ plan.display_name }}
|
||||
</h6>
|
||||
<span class="badge {% if plan.is_active %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{% if plan.is_active %}上架{% else %}下架{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="price-display text-primary mb-2">
|
||||
{% if plan.original_price and plan.original_price > plan.price %}
|
||||
<small class="text-muted d-block" style="font-size: 1rem;">
|
||||
<del>¥{{ plan.original_price }}</del>
|
||||
</small>
|
||||
{% endif %}
|
||||
¥{{ plan.price }}
|
||||
</div>
|
||||
<div class="text-muted mb-3">
|
||||
/ {% if plan.period == 'month' %}月{% elif plan.period == 'quarter' %}季{% elif plan.period == 'year' %}年{% else %}{{ plan.period_days }}天{% endif %}
|
||||
</div>
|
||||
|
||||
{% if plan.description %}
|
||||
<p class="text-muted small mb-3">{{ plan.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if plan.user_type_key %}
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">购买后升级为:</small>
|
||||
<span class="badge bg-secondary">{{ plan.user_type_key }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-sm text-start">
|
||||
<tr>
|
||||
<td><strong>套餐标识</strong></td>
|
||||
<td><code>{{ plan.plan_key }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>周期天数</strong></td>
|
||||
<td>{{ plan.period_days }} 天</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="d-flex gap-2 justify-content-center mt-3">
|
||||
<a href="{{ url_for('admin.edit_membership_plan', plan_id=plan.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> 编辑
|
||||
</a>
|
||||
{% if not plan.is_system %}
|
||||
<button onclick="deletePlan({{ plan.id }})"
|
||||
class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> 删除
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="togglePlan({{ plan.id }})"
|
||||
class="btn btn-sm btn-outline-{% if plan.is_active %}secondary{% else %}success{% endif %}">
|
||||
<i class="bi bi-power"></i> {% if plan.is_active %}下架{% else %}上架{% endif %}
|
||||
</button>
|
||||
<button onclick="recommendPlan({{ plan.id }})"
|
||||
class="btn btn-sm btn-outline-warning">
|
||||
<i class="bi bi-star{% if plan.is_recommended %}-fill{% endif %}"></i>
|
||||
{% if plan.is_recommended %}取消推荐{% else %}推荐{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not plans %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<p class="text-muted">数据库中暂无会员套餐配置</p>
|
||||
<button onclick="initDefaultPlans()" class="btn btn-primary">
|
||||
<i class="bi bi-download"></i> 初始化默认配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function deletePlan(planId) {
|
||||
if (!confirm('确定要删除此会员套餐?此操作不可恢复!')) return;
|
||||
|
||||
fetch(`/admin/membership-plans/${planId}/delete`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('删除失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function togglePlan(planId) {
|
||||
fetch(`/admin/membership-plans/${planId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('操作失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recommendPlan(planId) {
|
||||
fetch(`/admin/membership-plans/${planId}/recommend`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('操作失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initDefaultPlans() {
|
||||
if (!confirm('确定要初始化默认会员套餐配置?')) return;
|
||||
|
||||
fetch('/admin/membership-plans/init', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('初始化失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,6 +11,8 @@
|
||||
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.main-content { margin-left: 250px; padding: 20px; }
|
||||
.config-card { border-radius: 10px; transition: all 0.3s; }
|
||||
.config-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -23,6 +25,7 @@
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.packages') }}"><i class="bi bi-box-seam"></i> 数据包套餐</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||
@@ -34,78 +37,101 @@
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="row">
|
||||
<h4 class="mb-4"><i class="bi bi-sliders"></i> 系统配置</h4>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h6 class="mb-0"><i class="bi bi-sliders"></i> 用户权限配置</h6></div>
|
||||
<div class="card config-card h-100">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-person-badge"></i> 用户类型配置</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>用户类型</th>
|
||||
<th>每日次数</th>
|
||||
<th>最大页数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for type, limits in user_limits.items() %}
|
||||
<tr>
|
||||
<td>{{ type }}</td>
|
||||
<td>{{ limits.daily_translations if limits.daily_translations > 0 else '无限' }}</td>
|
||||
<td>{{ limits.max_pages if limits.max_pages > 0 else '无限' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<small class="text-muted">权限配置需修改 config.py 文件</small>
|
||||
<p class="text-muted">配置不同用户类型的权限限制,包括翻译次数、页数限制、功能权限等。</p>
|
||||
<p><strong>支持操作:</strong></p>
|
||||
<ul class="small">
|
||||
<li>添加新的用户类型</li>
|
||||
<li>编辑现有类型的权限</li>
|
||||
<li>删除自定义类型(系统类型不可删除)</li>
|
||||
<li>启用/禁用用户类型</li>
|
||||
</ul>
|
||||
<a href="{{ url_for('admin.user_types') }}" class="btn btn-primary">
|
||||
<i class="bi bi-gear"></i> 管理用户类型
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h6 class="mb-0"><i class="bi bi-credit-card"></i> 会员套餐配置</h6></div>
|
||||
<div class="card config-card h-100">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-credit-card"></i> 会员套餐配置</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>套餐</th>
|
||||
<th>价格</th>
|
||||
<th>周期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, plan in membership_plans.items() %}
|
||||
<tr>
|
||||
<td>{{ plan.name }}</td>
|
||||
<td>¥{{ plan.price }}</td>
|
||||
<td>{{ plan.period }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<small class="text-muted">套餐配置需修改 config.py 文件</small>
|
||||
<p class="text-muted">配置会员套餐的价格、周期、描述等,用户购买后可升级用户类型。</p>
|
||||
<p><strong>支持操作:</strong></p>
|
||||
<ul class="small">
|
||||
<li>添加新的会员套餐</li>
|
||||
<li>编辑套餐价格和描述</li>
|
||||
<li>删除自定义套餐(系统套餐不可删除)</li>
|
||||
<li>上架/下架套餐</li>
|
||||
<li>设置推荐套餐</li>
|
||||
</ul>
|
||||
<a href="{{ url_for('admin.membership_plans') }}" class="btn btn-primary">
|
||||
<i class="bi bi-credit-card"></i> 管理会员套餐
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card config-card h-100">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-box-seam"></i> 数据包套餐</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">管理数据包套餐,用户可以购买额外的翻译次数。</p>
|
||||
<a href="{{ url_for('admin.packages') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-box-seam"></i> 管理数据包
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card config-card h-100">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-cpu"></i> 大模型配置</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">配置翻译使用的LLM大模型API地址、模型名称等参数。</p>
|
||||
<p><strong>当前模型:</strong> {{ llm_config.model }}</p>
|
||||
<a href="{{ url_for('admin.llm_config') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-cpu"></i> 配置大模型
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h6 class="mb-0"><i class="bi bi-info-circle"></i> 系统信息</h6></div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<p><strong>应用名称:</strong> PDF翻译助手</p>
|
||||
<p><strong>版本:</strong> 1.0.0</p>
|
||||
<p><strong>版本:</strong> 2.0.0</p>
|
||||
<p><strong>框架:</strong> Flask + SQLAlchemy</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p><strong>LLM模型:</strong> qwen/qwen3.5-35b-a3b</p>
|
||||
<p><strong>API地址:</strong> http://192.168.2.5:1234/v1</p>
|
||||
<p><strong>缓存有效期:</strong> 30天</p>
|
||||
<p><strong>默认最大文件:</strong> 50MB</p>
|
||||
<p><strong>数据库:</strong> SQLite</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p><strong>缓存有效期:</strong> 30天</p>
|
||||
<p><strong>最大文件:</strong> 50MB</p>
|
||||
<p><strong>API地址:</strong> {{ llm_config.api_base }}</p>
|
||||
<p><strong>超时时间:</strong> {{ llm_config.timeout }}秒</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
198
templates/admin/user_type_form.html
Normal file
198
templates/admin/user_type_form.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% if user_type %}编辑用户类型{% else %}添加用户类型{% endif %} - 后台管理</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #f5f5f5; }
|
||||
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
|
||||
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.main-content { margin-left: 250px; padding: 20px; }
|
||||
.feature-checkbox { margin: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
|
||||
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
|
||||
</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.packages') }}"><i class="bi bi-box-seam"></i> 数据包套餐</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
|
||||
</ul>
|
||||
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
|
||||
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4>
|
||||
<i class="bi bi-person-badge"></i>
|
||||
{% if user_type %}编辑用户类型{% else %}添加用户类型{% endif %}
|
||||
</h4>
|
||||
<a href="{{ url_for('admin.user_types') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> 返回列表
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="typeForm">
|
||||
{% if user_type %}
|
||||
<input type="hidden" name="type_id" value="{{ user_type.id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">类型标识 <small class="text-muted">(唯一标识符)</small></label>
|
||||
{% if user_type %}
|
||||
<input type="text" class="form-control" name="type_key"
|
||||
value="{{ user_type.type_key }}" readonly>
|
||||
<small class="text-muted">类型标识创建后不可修改</small>
|
||||
{% else %}
|
||||
<input type="text" class="form-control" name="type_key"
|
||||
placeholder="例如: vip_special" required>
|
||||
<small class="text-muted">建议使用英文,如 vip_special, trial_user</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">显示名称</label>
|
||||
<input type="text" class="form-control" name="display_name"
|
||||
value="{{ user_type.display_name if user_type else '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">排序权重</label>
|
||||
<input type="number" class="form-control" name="sort_order"
|
||||
value="{{ user_type.sort_order if user_type else 0 }}">
|
||||
<small class="text-muted">数字越小排在前面</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">每日翻译次数</label>
|
||||
<input type="number" class="form-control" name="daily_translations"
|
||||
value="{{ user_type.daily_translations if user_type else 10 }}">
|
||||
<small class="text-muted">输入 -1 表示无限制</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">单文件最大页数</label>
|
||||
<input type="number" class="form-control" name="max_pages"
|
||||
value="{{ user_type.max_pages if user_type else 50 }}">
|
||||
<small class="text-muted">输入 -1 表示无限制</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">最大文件大小 (MB)</label>
|
||||
<input type="number" class="form-control" name="max_file_size_mb"
|
||||
value="{{ (user_type.max_file_size / 1024 / 1024) | round(0) if user_type else 30 }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">状态</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_active"
|
||||
id="is_active" {% if user_type and user_type.is_active %}checked{% elif not user_type %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_active">启用此用户类型</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">功能权限</label>
|
||||
<div class="row">
|
||||
{% for feature in all_features %}
|
||||
<div class="col-md-3">
|
||||
<div class="form-check feature-checkbox">
|
||||
<input class="form-check-input" type="checkbox" name="features"
|
||||
value="{{ feature.key }}" id="feature_{{ feature.key }}"
|
||||
{% if user_type and feature.key in user_type.get_features() %}checked{% endif %}>
|
||||
<label class="form-check-label" for="feature_{{ feature.key }}">
|
||||
{{ feature.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> {% if user_type %}保存修改{% else %}添加类型{% endif %}
|
||||
</button>
|
||||
<a href="{{ url_for('admin.user_types') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle"></i> 取消
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.getElementById('typeForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
// 收集选中的功能
|
||||
const features = [];
|
||||
document.querySelectorAll('input[name="features"]:checked').forEach(el => {
|
||||
features.push(el.value);
|
||||
});
|
||||
|
||||
const data = {
|
||||
type_key: formData.get('type_key'),
|
||||
display_name: formData.get('display_name'),
|
||||
daily_translations: parseInt(formData.get('daily_translations')),
|
||||
max_pages: parseInt(formData.get('max_pages')),
|
||||
max_file_size_mb: parseInt(formData.get('max_file_size_mb')),
|
||||
sort_order: parseInt(formData.get('sort_order')),
|
||||
is_active: formData.get('is_active') === 'on',
|
||||
features: JSON.stringify(features)
|
||||
};
|
||||
|
||||
{% if user_type %}
|
||||
const url = `/admin/user-types/{{ user_type.id }}/edit`;
|
||||
{% else %}
|
||||
const url = '/admin/user-types/add';
|
||||
{% endif %}
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
{% if user_type %}
|
||||
alert('用户类型已更新');
|
||||
{% else %}
|
||||
alert('用户类型已添加');
|
||||
{% endif %}
|
||||
window.location.href = '/admin/user-types';
|
||||
} else {
|
||||
alert('操作失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
206
templates/admin/user_types.html
Normal file
206
templates/admin/user_types.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>用户类型配置 - 后台管理</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #f5f5f5; }
|
||||
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
|
||||
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.main-content { margin-left: 250px; padding: 20px; }
|
||||
.type-card { border-radius: 10px; transition: all 0.3s; }
|
||||
.type-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.type-card.guest { border-top: 4px solid #6c757d; }
|
||||
.type-card.free { border-top: 4px solid #28a745; }
|
||||
.type-card.vip { border-top: 4px solid #ffc107; }
|
||||
.type-card.enterprise { border-top: 4px solid #0d6efd; }
|
||||
.badge-system { background: #dc3545; }
|
||||
.badge-custom { background: #17a2b8; }
|
||||
.feature-tag { font-size: 0.75rem; margin: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
|
||||
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
|
||||
</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.packages') }}"><i class="bi bi-box-seam"></i> 数据包套餐</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
|
||||
</ul>
|
||||
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
|
||||
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4><i class="bi bi-person-badge"></i> 用户类型配置</h4>
|
||||
<div>
|
||||
<a href="{{ url_for('admin.add_user_type') }}" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> 添加用户类型
|
||||
</a>
|
||||
<a href="{{ url_for('admin.membership_plans') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-credit-card"></i> 会员套餐配置
|
||||
</a>
|
||||
<a href="{{ url_for('admin.settings') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-gear"></i> 基础设置
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% for user_type in user_types %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card type-card {% if user_type.type_key == 'guest' %}guest{% elif user_type.type_key == 'free' %}free{% elif user_type.type_key.startswith('vip_') and not user_type.type_key.endswith('_enterprise') %}vip{% elif user_type.type_key.endswith('_enterprise') or user_type.type_key.endswith('enterprise') %}enterprise{% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<span class="badge {% if user_type.is_system %}badge-system{% else %}badge-custom{% endif %} me-2">
|
||||
{% if user_type.is_system %}系统{% else %}自定义{% endif %}
|
||||
</span>
|
||||
{{ user_type.display_name }}
|
||||
</h6>
|
||||
<span class="badge {% if user_type.is_active %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{% if user_type.is_active %}启用{% else %}禁用{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>类型标识</strong></td>
|
||||
<td><code>{{ user_type.type_key }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>每日翻译</strong></td>
|
||||
<td>
|
||||
{% if user_type.daily_translations < 0 %}
|
||||
<span class="text-success">无限制</span>
|
||||
{% else %}
|
||||
{{ user_type.daily_translations }} 次
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>最大页数</strong></td>
|
||||
<td>
|
||||
{% if user_type.max_pages < 0 %}
|
||||
<span class="text-success">无限制</span>
|
||||
{% else %}
|
||||
{{ user_type.max_pages }} 页
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>最大文件</strong></td>
|
||||
<td>{{ (user_type.max_file_size / 1024 / 1024) | round(0) }} MB</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>功能权限:</strong>
|
||||
</div>
|
||||
<div>
|
||||
{% for feature in user_type.get_features() %}
|
||||
<span class="badge bg-info feature-tag">{{ feature }}</span>
|
||||
{% endfor %}
|
||||
{% if not user_type.get_features() %}
|
||||
<span class="text-muted">无特殊功能</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<a href="{{ url_for('admin.edit_user_type', type_id=user_type.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> 编辑
|
||||
</a>
|
||||
{% if not user_type.is_system %}
|
||||
<button onclick="deleteType({{ user_type.id }})"
|
||||
class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> 删除
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="toggleType({{ user_type.id }})"
|
||||
class="btn btn-sm btn-outline-{% if user_type.is_active %}secondary{% else %}success{% endif %}">
|
||||
<i class="bi bi-power"></i> {% if user_type.is_active %}禁用{% else %}启用{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not user_types %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<p class="text-muted">数据库中暂无用户类型配置</p>
|
||||
<button onclick="initDefaultTypes()" class="btn btn-primary">
|
||||
<i class="bi bi-download"></i> 初始化默认配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function deleteType(typeId) {
|
||||
if (!confirm('确定要删除此用户类型?此操作不可恢复!')) return;
|
||||
|
||||
fetch(`/admin/user-types/${typeId}/delete`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('删除失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleType(typeId) {
|
||||
fetch(`/admin/user-types/${typeId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('操作失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initDefaultTypes() {
|
||||
if (!confirm('确定要初始化默认用户类型配置?')) return;
|
||||
|
||||
fetch('/admin/user-types/init', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('初始化失败: ' + result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user