4 Commits

Author SHA1 Message Date
8c35a8741a feat: 系统配置支持动态增删用户类型和会员套餐
新增功能:
- UserTypeConfig 模型:用户类型配置支持动态增删
- MembershipPlanConfig 模型:会员套餐配置支持动态增删
- 用户类型管理页面:添加、编辑、删除、启用/禁用用户类型
- 会员套餐管理页面:添加、编辑、删除、上架/下架、推荐套餐
- 功能权限配置:支持选择功能列表
- 初始化默认配置功能

改进:
- settings.html 页面重构,提供配置入口链接
- 新增API接口支持增删改查操作
2026-04-11 10:25:03 +08:00
329e795648 fix: settings页面显示动态LLM配置 2026-04-10 23:18:21 +08:00
7fede0212b feat: 后台添加大模型配置管理页面
- 新增 /admin/llm_config 页面
- 支持配置API地址、Key、模型名称、参数
- 支持测试连接和恢复默认配置
- 配置保存到数据库,翻译服务动态读取
- 所有后台页面侧边栏添加入口
2026-04-10 18:42:20 +08:00
801dd1e29b fix: 首页加载时检查日期并重置daily_count 2026-04-10 18:29:37 +08:00
22 changed files with 1832 additions and 53 deletions

562
admin.py
View File

@@ -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
)
@@ -661,4 +666,555 @@ def api_user_add_package(user_id):
'id': user_package.id,
'name': user_package.package_name,
'remaining': user_package.remaining_count
}})
}})
# ==================== LLM大模型配置 ====================
@admin_bp.route('/llm_config')
@admin_required
def llm_config():
"""LLM配置页面"""
from config import LLM_CONFIG
# 从数据库获取配置,如果没有则使用默认值
config = {
'api_base': DynamicConfig.get('llm_api_base', LLM_CONFIG.get('api_base')),
'api_key': DynamicConfig.get('llm_api_key', LLM_CONFIG.get('api_key')),
'model': DynamicConfig.get('llm_model', LLM_CONFIG.get('model')),
'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')),
}
return render_template('admin/llm_config.html', config=config)
@admin_bp.route('/llm_config/save', methods=['POST'])
@admin_required
def save_llm_config():
"""保存LLM配置"""
data = request.json
DynamicConfig.set('llm_api_base', data.get('api_base'), category='llm', user_id=session.get('user_id'))
DynamicConfig.set('llm_api_key', data.get('api_key'), category='llm', user_id=session.get('user_id'))
DynamicConfig.set('llm_model', data.get('model'), category='llm', user_id=session.get('user_id'))
DynamicConfig.set('llm_max_tokens', data.get('max_tokens'), category='llm', value_type='int', user_id=session.get('user_id'))
DynamicConfig.set('llm_chunk_size', data.get('chunk_size'), category='llm', value_type='int', user_id=session.get('user_id'))
DynamicConfig.set('llm_timeout', data.get('timeout'), category='llm', value_type='int', user_id=session.get('user_id'))
# 记录日志
log = OperationLog(
user_id=session.get('user_id'),
username='admin',
action='update_llm_config',
detail='更新大模型配置'
)
db.session.add(log)
db.session.commit()
return jsonify({'success': True})
@admin_bp.route('/llm_config/test', methods=['POST'])
@admin_required
def test_llm_connection():
"""测试LLM连接"""
data = request.json
try:
from openai import OpenAI
client = OpenAI(
api_key=data.get('api_key', 'sk-test'),
base_url=data.get('api_base'),
)
# 发送简单测试请求
response = client.chat.completions.create(
model=data.get('model'),
messages=[{"role": "user", "content": "Hello"}],
max_tokens=10,
timeout=10,
)
return jsonify({
'success': True,
'model': data.get('model'),
'response': response.choices[0].message.content[:50] if response.choices else 'OK'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@admin_bp.route('/llm_config/reset', methods=['POST'])
@admin_required
def reset_llm_config():
"""恢复默认LLM配置"""
from config import LLM_CONFIG
# 删除数据库中的LLM配置
DynamicConfig.query.filter_by(category='llm').delete()
db.session.commit()
return jsonify({'success': True})
# ==================== 获取当前LLM配置供其他模块使用 ====================
def get_llm_config():
"""获取当前LLM配置"""
from config import LLM_CONFIG
return {
'api_base': DynamicConfig.get('llm_api_base', LLM_CONFIG.get('api_base')),
'api_key': DynamicConfig.get('llm_api_key', LLM_CONFIG.get('api_key')),
'model': DynamicConfig.get('llm_model', LLM_CONFIG.get('model')),
'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]

18
app.py
View File

@@ -93,11 +93,29 @@ def index():
"""首页"""
user = get_current_user()
if user:
# 检查日期并重置计数(使用上海时区)
from datetime import timezone, timedelta
shanghai_tz = timezone(timedelta(hours=8))
today = datetime.now(shanghai_tz).date()
if user.last_translate_date != today:
user.daily_count = 0
user.last_translate_date = today
db.session.commit()
limits = USER_LIMITS.get(user.user_type, USER_LIMITS['free'])
daily_remaining = limits['daily_translations'] - user.daily_count if limits['daily_translations'] > 0 else '无限'
max_pages = limits['max_pages'] if limits['max_pages'] > 0 else '无限'
else:
guest = get_or_create_guest()
# 检查日期并重置访客计数
from datetime import timezone, timedelta
shanghai_tz = timezone(timedelta(hours=8))
today = datetime.now(shanghai_tz).date()
if guest.last_translate_date != today:
guest.daily_count = 0
guest.last_translate_date = today
db.session.commit()
limits = USER_LIMITS['guest']
daily_remaining = limits['daily_translations'] - guest.daily_count
max_pages = limits['max_pages']

105
models.py
View File

@@ -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,
}

View File

@@ -274,6 +274,13 @@ class TranslationTask:
# 启动翻译线程
def run_translation():
# 动态获取LLM配置
if app:
with app.app_context():
from admin import get_llm_config
llm_config = get_llm_config()
config['LLM_CONFIG'] = llm_config
service = TranslationService(config)
task['status'] = 'processing'
task['started_at'] = datetime.now().isoformat()

View File

@@ -25,6 +25,7 @@
<li class="nav-item"><a class="nav-link active" 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.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" 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">

View File

@@ -93,6 +93,11 @@
<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" href="{{ url_for('admin.settings') }}">
<i class="bi bi-sliders"></i> 系统配置

View File

@@ -0,0 +1,187 @@
<!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; }
</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.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 active" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
<li class="nav-item"><a class="nav-link" 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="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-cpu"></i> 大模型接口配置</h6>
</div>
<div class="card-body">
<form id="llmConfigForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">API 地址</label>
<input type="text" class="form-control" name="api_base" value="{{ config.api_base }}" placeholder="http://localhost:1234/v1">
<small class="text-muted">LLM服务的API endpoint</small>
</div>
<div class="mb-3">
<label class="form-label">API Key</label>
<input type="text" class="form-control" name="api_key" value="{{ config.api_key }}" placeholder="sk-xxx">
<small class="text-muted">如果不需要可留空</small>
</div>
<div class="mb-3">
<label class="form-label">模型名称</label>
<input type="text" class="form-control" name="model" value="{{ config.model }}" placeholder="qwen/qwen3.5-35b">
<small class="text-muted">使用的模型ID</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">最大输出Token</label>
<input type="number" class="form-control" name="max_tokens" value="{{ config.max_tokens }}" placeholder="8000">
<small class="text-muted">每次翻译最大输出长度</small>
</div>
<div class="mb-3">
<label class="form-label">分块大小</label>
<input type="number" class="form-control" name="chunk_size" value="{{ config.chunk_size }}" placeholder="2000">
<small class="text-muted">PDF文本分块大小</small>
</div>
<div class="mb-3">
<label class="form-label">超时时间(秒)</label>
<input type="number" class="form-control" name="timeout" value="{{ config.timeout }}" placeholder="180">
<small class="text-muted">API请求超时时间</small>
</div>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> 保存配置</button>
<button type="button" class="btn btn-outline-secondary" onclick="testConnection()"><i class="bi bi-plug"></i> 测试连接</button>
<button type="button" class="btn btn-outline-warning" onclick="resetToDefault()"><i class="bi bi-arrow-counterclockwise"></i> 恢复默认</button>
</div>
</form>
<div id="testResult" class="mt-3" style="display:none;"></div>
</div>
</div>
<div class="card mt-3">
<div class="card-header"><h6 class="mb-0"><i class="bi bi-info-circle"></i> 常用模型配置参考</h6></div>
<div class="card-body">
<table class="table table-sm">
<thead class="table-light">
<tr><th>服务商</th><th>API地址</th><th>模型示例</th></tr>
</thead>
<tbody>
<tr><td>本地LM Studio</td><td>http://localhost:1234/v1</td><td>根据加载的模型</td></tr>
<tr><td>OpenAI</td><td>https://api.openai.com/v1</td><td>gpt-4, gpt-3.5-turbo</td></tr>
<tr><td>DeepSeek</td><td>https://api.deepseek.com/v1</td><td>deepseek-chat</td></tr>
<tr><td>阿里百炼</td><td>https://dashscope.aliyuncs.com/compatible-mode/v1</td><td>qwen-turbo, qwen-plus</td></tr>
<tr><td>SiliconFlow</td><td>https://api.siliconflow.cn/v1</td><td>Qwen/Qwen2.5-72B-Instruct</td></tr>
</tbody>
</table>
</div>
</div>
</main>
<script>
document.getElementById('llmConfigForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {};
formData.forEach((value, key) => {
if (key === 'max_tokens' || key === 'chunk_size' || key === 'timeout') {
data[key] = parseInt(value) || 0;
} else {
data[key] = value;
}
});
fetch('/admin/llm_config/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
if (res.success) {
alert('配置已保存!');
} else {
alert('保存失败: ' + res.error);
}
})
.catch(err => alert('请求失败: ' + err));
});
function testConnection() {
const formData = new FormData(document.getElementById('llmConfigForm'));
const data = {};
formData.forEach((value, key) => {
if (key === 'max_tokens' || key === 'chunk_size' || key === 'timeout') {
data[key] = parseInt(value) || 0;
} else {
data[key] = value;
}
});
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="alert alert-info">正在测试连接...</div>';
fetch('/admin/llm_config/test', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
if (res.success) {
resultDiv.innerHTML = '<div class="alert alert-success"><i class="bi bi-check-circle"></i> 连接成功!模型: ' + res.model + '</div>';
} else {
resultDiv.innerHTML = '<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ' + res.error + '</div>';
}
})
.catch(err => {
resultDiv.innerHTML = '<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 请求失败: ' + err + '</div>';
});
}
function resetToDefault() {
if (confirm('确定恢复默认配置吗?')) {
fetch('/admin/llm_config/reset', {method: 'POST'})
.then(r => r.json())
.then(res => {
if (res.success) {
location.reload();
} else {
alert('恢复失败: ' + res.error);
}
});
}
}
</script>
</body>
</html>

View File

@@ -25,6 +25,7 @@
<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.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
<li class="nav-item"><a class="nav-link active" 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" 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">

View File

@@ -31,6 +31,7 @@
<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">

View 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>

View 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>

View File

@@ -26,6 +26,7 @@
<li class="nav-item"><a class="nav-link active" 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" 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">

View File

@@ -30,6 +30,7 @@
<li class="nav-item"><a class="nav-link active" 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" 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">

View File

@@ -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,8 +25,10 @@
<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">
@@ -33,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>

View File

@@ -26,6 +26,7 @@
<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 active" 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" 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">

View File

@@ -26,6 +26,7 @@
<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.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" 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">

View File

@@ -26,6 +26,7 @@
<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.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" 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">

View File

@@ -27,6 +27,7 @@
<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.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" 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">

View File

@@ -26,6 +26,7 @@
<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">

View 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>

View 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>

View File

@@ -28,6 +28,7 @@
<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.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" 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">