diff --git a/backend/app/models/game.py b/backend/app/models/game.py
index ad9c222..02e0f2b 100644
--- a/backend/app/models/game.py
+++ b/backend/app/models/game.py
@@ -56,3 +56,18 @@ class Bet(Base):
# 关联关系
chest = relationship("Chest", back_populates="bets", foreign_keys=[chest_id])
+
+
+class ChestTemplate(Base):
+ """宝箱模板表"""
+ __tablename__ = "chest_templates"
+
+ id = Column(Integer, primary_key=True, autoincrement=True, index=True)
+ streamer_id = Column(Integer, nullable=False, index=True, comment="主播ID")
+ name = Column(String(100), nullable=False, comment="模板名称")
+ title = Column(String(200), nullable=False, comment="宝箱标题")
+ option_a = Column(String(100), nullable=False, comment="选项A")
+ option_b = Column(String(100), nullable=False, comment="选项B")
+ countdown_seconds = Column(Integer, default=300, comment="倒计时(秒)")
+ created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
diff --git a/backend/app/routers/game.py b/backend/app/routers/game.py
index f526fd4..83f7e6b 100644
--- a/backend/app/routers/game.py
+++ b/backend/app/routers/game.py
@@ -8,7 +8,8 @@ from datetime import datetime
from ..core.database import get_db
from ..schemas.game import (
ChestCreate, ChestResponse, BetCreate, BetResponse,
- ChestSettle, ChestLock
+ ChestSettle, ChestLock,
+ ChestTemplateCreate, ChestTemplateUpdate, ChestTemplateResponse
)
from ..services.game_service import GameService
from ..models.game import ChestStatus
@@ -252,3 +253,66 @@ def get_chest_bets(
获取宝箱下注记录
"""
return GameService.get_chest_bets(db, chest_id)
+
+
+# ==================== 宝箱模板相关路由 ====================
+
+@router.get("/templates", response_model=List[ChestTemplateResponse])
+def get_templates(
+ db: Session = Depends(get_db),
+ current_user: get_current_streamer = Depends(get_current_streamer)
+):
+ """
+ 获取当前主播的宝箱模板列表
+ """
+ templates = GameService.get_templates_by_streamer(db, current_user.id)
+ return templates
+
+
+@router.post("/templates", response_model=ChestTemplateResponse)
+def create_template(
+ template_data: ChestTemplateCreate,
+ db: Session = Depends(get_db),
+ current_user: get_current_streamer = Depends(get_current_streamer)
+):
+ """
+ 创建宝箱模板
+ """
+ try:
+ template = GameService.create_template(db, current_user.id, template_data)
+ return template
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@router.put("/templates/{template_id}", response_model=ChestTemplateResponse)
+def update_template(
+ template_id: int,
+ template_data: ChestTemplateUpdate,
+ db: Session = Depends(get_db),
+ current_user: get_current_streamer = Depends(get_current_streamer)
+):
+ """
+ 更新宝箱模板
+ """
+ try:
+ template = GameService.update_template(db, template_id, current_user.id, template_data)
+ return template
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@router.delete("/templates/{template_id}")
+def delete_template(
+ template_id: int,
+ db: Session = Depends(get_db),
+ current_user: get_current_streamer = Depends(get_current_streamer)
+):
+ """
+ 删除宝箱模板
+ """
+ try:
+ GameService.delete_template(db, template_id, current_user.id)
+ return {"message": "删除成功"}
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
diff --git a/backend/app/schemas/game.py b/backend/app/schemas/game.py
index 89a4c5a..f27a01b 100644
--- a/backend/app/schemas/game.py
+++ b/backend/app/schemas/game.py
@@ -79,4 +79,40 @@ class ChestSettle(BaseModel):
class ChestLock(BaseModel):
"""宝箱封盘"""
- pass
\ No newline at end of file
+ pass
+
+
+# ==================== 宝箱模板相关 schemas ====================
+
+class ChestTemplateBase(BaseModel):
+ """宝箱模板基础模型"""
+ name: str = Field(..., min_length=1, max_length=100, description="模板名称")
+ title: str = Field(..., min_length=1, max_length=200, description="宝箱标题")
+ option_a: str = Field(..., min_length=1, max_length=100, description="选项A")
+ option_b: str = Field(..., min_length=1, max_length=100, description="选项B")
+ countdown_seconds: int = Field(default=300, ge=10, le=3600, description="倒计时(秒)")
+
+
+class ChestTemplateCreate(ChestTemplateBase):
+ """创建宝箱模板"""
+ pass
+
+
+class ChestTemplateUpdate(BaseModel):
+ """更新宝箱模板"""
+ name: Optional[str] = Field(None, min_length=1, max_length=100, description="模板名称")
+ title: Optional[str] = Field(None, min_length=1, max_length=200, description="宝箱标题")
+ option_a: Optional[str] = Field(None, min_length=1, max_length=100, description="选项A")
+ option_b: Optional[str] = Field(None, min_length=1, max_length=100, description="选项B")
+ countdown_seconds: Optional[int] = Field(None, ge=10, le=3600, description="倒计时(秒)")
+
+
+class ChestTemplateResponse(ChestTemplateBase):
+ """宝箱模板响应模型"""
+ id: int
+ streamer_id: int
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
\ No newline at end of file
diff --git a/backend/app/services/game_service.py b/backend/app/services/game_service.py
index 751fa72..db130c5 100644
--- a/backend/app/services/game_service.py
+++ b/backend/app/services/game_service.py
@@ -614,4 +614,105 @@ class GameService:
"""
获取宝箱下注记录
"""
- return db.query(Bet).filter(Bet.chest_id == chest_id).all()
\ No newline at end of file
+ return db.query(Bet).filter(Bet.chest_id == chest_id).all()
+
+ # ==================== 宝箱模板相关方法 ====================
+
+ @staticmethod
+ def get_templates_by_streamer(db: Session, streamer_id: int) -> List:
+ """
+ 获取指定主播的所有宝箱模板
+ """
+ from ..models.game import ChestTemplate
+ return db.query(ChestTemplate).filter(
+ ChestTemplate.streamer_id == streamer_id
+ ).order_by(ChestTemplate.created_at.desc()).all()
+
+ @staticmethod
+ def get_template_by_id(db: Session, template_id: int, streamer_id: int):
+ """
+ 根据ID获取宝箱模板(验证所有权)
+ """
+ from ..models.game import ChestTemplate
+ return db.query(ChestTemplate).filter(
+ and_(
+ ChestTemplate.id == template_id,
+ ChestTemplate.streamer_id == streamer_id
+ )
+ ).first()
+
+ @staticmethod
+ def create_template(db: Session, streamer_id: int, template_data) -> 'ChestTemplate':
+ """
+ 创建宝箱模板
+ """
+ from ..models.game import ChestTemplate
+
+ # 检查模板数量限制(最多3个)
+ existing_count = db.query(ChestTemplate).filter(
+ ChestTemplate.streamer_id == streamer_id
+ ).count()
+
+ if existing_count >= 3:
+ raise ValueError("模板数量已达上限(3个)")
+
+ # 创建模板
+ db_template = ChestTemplate(
+ streamer_id=streamer_id,
+ name=template_data.name,
+ title=template_data.title,
+ option_a=template_data.option_a,
+ option_b=template_data.option_b,
+ countdown_seconds=template_data.countdown_seconds
+ )
+ db.add(db_template)
+ db.commit()
+ db.refresh(db_template)
+
+ return db_template
+
+ @staticmethod
+ def update_template(db: Session, template_id: int, streamer_id: int, template_data) -> 'ChestTemplate':
+ """
+ 更新宝箱模板
+ """
+ from ..models.game import ChestTemplate
+
+ # 查找模板并验证所有权
+ db_template = GameService.get_template_by_id(db, template_id, streamer_id)
+ if not db_template:
+ raise ValueError("模板不存在或无权操作")
+
+ # 更新字段
+ if template_data.name is not None:
+ db_template.name = template_data.name
+ if template_data.title is not None:
+ db_template.title = template_data.title
+ if template_data.option_a is not None:
+ db_template.option_a = template_data.option_a
+ if template_data.option_b is not None:
+ db_template.option_b = template_data.option_b
+ if template_data.countdown_seconds is not None:
+ db_template.countdown_seconds = template_data.countdown_seconds
+
+ db.commit()
+ db.refresh(db_template)
+
+ return db_template
+
+ @staticmethod
+ def delete_template(db: Session, template_id: int, streamer_id: int) -> bool:
+ """
+ 删除宝箱模板
+ """
+ from ..models.game import ChestTemplate
+
+ # 查找模板并验证所有权
+ db_template = GameService.get_template_by_id(db, template_id, streamer_id)
+ if not db_template:
+ raise ValueError("模板不存在或无权操作")
+
+ db.delete(db_template)
+ db.commit()
+
+ return True
\ No newline at end of file
diff --git a/frontend/dist/index.html b/frontend/dist/index.html
index 3687c8e..24efc3c 100644
--- a/frontend/dist/index.html
+++ b/frontend/dist/index.html
@@ -5,7 +5,7 @@
互动竞猜开宝箱系统
-
+
diff --git a/frontend/src/pages/StreamerConsole.tsx b/frontend/src/pages/StreamerConsole.tsx
index 91deeed..38f4e8f 100644
--- a/frontend/src/pages/StreamerConsole.tsx
+++ b/frontend/src/pages/StreamerConsole.tsx
@@ -5,7 +5,7 @@ import { useAuth } from '../contexts/AuthContext';
import { websocketService } from '../services/websocket';
import Card from '../components/Card';
import Loading from '../components/Loading';
-import type { Chest } from '../types';
+import type { Chest, ChestTemplate } from '../types';
const StreamerConsole = () => {
const { user } = useAuth();
@@ -34,6 +34,18 @@ const StreamerConsole = () => {
const [countdowns, setCountdowns] = useState<{[key: number]: number}>({});
const countdownIntervalsRef = useRef<{[key: number]: number}>({});
+ // 宝箱模板相关状态
+ const [templates, setTemplates] = useState([]);
+ const [showTemplateForm, setShowTemplateForm] = useState(false);
+ const [editingTemplate, setEditingTemplate] = useState(null);
+ const [templateName, setTemplateName] = useState('');
+ const [templateTitle, setTemplateTitle] = useState('');
+ const [templateOptionA, setTemplateOptionA] = useState('');
+ const [templateOptionB, setTemplateOptionB] = useState('');
+ const [templateCountdown, setTemplateCountdown] = useState(300);
+ const [selectedTemplateId, setSelectedTemplateId] = useState(null);
+ const [templateLoading, setTemplateLoading] = useState(false);
+
// ============ 计算值(在Hook之前)===========
const activeChests = myChests.filter(c => c.status === 0 || c.status === 1);
const finishedChests = myChests.filter(c => c.status === 3 || c.status === 4);
@@ -57,8 +69,29 @@ const StreamerConsole = () => {
}
loadMyChests();
+ loadTemplates();
}, [user, navigate]);
+ // ============ 加载宝箱模板 ============
+ const loadTemplates = async () => {
+ try {
+ console.log('正在加载模板...');
+ const token = localStorage.getItem('token');
+ console.log('Token存在:', !!token);
+ const data = await gameApi.getTemplates();
+ console.log('模板加载成功:', data);
+ setTemplates(data);
+ } catch (err: any) {
+ console.error('加载模板失败:', err);
+ console.error('错误详情:', err.response?.data);
+ console.error('状态码:', err.response?.status);
+ if (err.response?.status === 401) {
+ console.error('认证失败,请检查登录状态');
+ // 可以在这里添加重新登录的逻辑
+ }
+ }
+ };
+
// 监听WebSocket连接状态
useEffect(() => {
const interval = setInterval(() => {
@@ -186,6 +219,126 @@ const StreamerConsole = () => {
}
};
+ // ============ 宝箱模板管理函数 ============
+ const handleSaveTemplate = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!templateName || !templateTitle || !templateOptionA || !templateOptionB) {
+ setError('请填写完整信息');
+ return;
+ }
+
+ try {
+ setTemplateLoading(true);
+ setError('');
+ console.log('保存模板:', { templateName, templateTitle, templateOptionA, templateOptionB, templateCountdown });
+
+ if (editingTemplate) {
+ // 更新模板
+ console.log('更新模板 ID:', editingTemplate.id);
+ await gameApi.updateTemplate(editingTemplate.id, {
+ name: templateName,
+ title: templateTitle,
+ option_a: templateOptionA,
+ option_b: templateOptionB,
+ countdown_seconds: templateCountdown,
+ });
+ alert('模板更新成功!');
+ } else {
+ // 创建模板
+ console.log('创建新模板');
+ await gameApi.createTemplate({
+ name: templateName,
+ title: templateTitle,
+ option_a: templateOptionA,
+ option_b: templateOptionB,
+ countdown_seconds: templateCountdown,
+ });
+ alert('模板创建成功!');
+ }
+
+ // 重置表单
+ resetTemplateForm();
+ await loadTemplates();
+ } catch (err: any) {
+ console.error('保存模板失败:', err);
+ console.error('错误详情:', err.response?.data);
+ console.error('状态码:', err.response?.status);
+ setError(err.response?.data?.detail || '保存模板失败');
+ } finally {
+ setTemplateLoading(false);
+ }
+ };
+
+ const handleDeleteTemplate = async (templateId: number) => {
+ if (!confirm('确定要删除这个模板吗?')) {
+ return;
+ }
+
+ try {
+ setTemplateLoading(true);
+ await gameApi.deleteTemplate(templateId);
+ alert('模板删除成功!');
+ await loadTemplates();
+ } catch (err: any) {
+ setError(err.response?.data?.detail || '删除模板失败');
+ } finally {
+ setTemplateLoading(false);
+ }
+ };
+
+ const handleEditTemplate = (template: ChestTemplate) => {
+ setEditingTemplate(template);
+ setTemplateName(template.name);
+ setTemplateTitle(template.title);
+ setTemplateOptionA(template.option_a);
+ setTemplateOptionB(template.option_b);
+ setTemplateCountdown(template.countdown_seconds);
+ setShowTemplateForm(true);
+ };
+
+ const resetTemplateForm = () => {
+ setEditingTemplate(null);
+ setTemplateName('');
+ setTemplateTitle('');
+ setTemplateOptionA('');
+ setTemplateOptionB('');
+ setTemplateCountdown(300);
+ setShowTemplateForm(false);
+ };
+
+ const handleUseTemplate = async (template: ChestTemplate) => {
+ // 弹出确认框
+ const confirmMessage = `确定要使用模板"${template.name}"创建宝箱吗?
+
+模板内容:
+标题:${template.title}
+选项:${template.option_a} vs ${template.option_b}
+时长:${template.countdown_seconds}秒`;
+
+ if (!confirm(confirmMessage)) {
+ return; // 用户取消,不执行任何操作
+ }
+
+ try {
+ // 用户确认后,直接创建宝箱
+ await gameApi.createChest({
+ title: template.title,
+ option_a: template.option_a,
+ option_b: template.option_b,
+ countdown_seconds: template.countdown_seconds,
+ });
+
+ // 重新加载宝箱列表
+ await loadMyChests();
+
+ alert(`宝箱创建成功!\n模板:${template.name}`);
+ } catch (err: any) {
+ console.error('创建宝箱失败:', err);
+ alert(err.response?.data?.detail || '创建宝箱失败');
+ }
+ };
+
const handleLockChest = async (chestId: number) => {
if (!confirm('确定要封盘吗?封盘后将无法再下注。')) {
return;
@@ -261,9 +414,68 @@ const StreamerConsole = () => {
return ;
}
+ // ============ 样式 ============
+ const templateStyles = `
+ .template-form {
+ background: #f8f9fa;
+ padding: 1.5rem;
+ border-radius: 8px;
+ margin-bottom: 1rem;
+ }
+
+ .templates-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .template-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ border: 1px solid #dee2e6;
+ border-radius: 8px;
+ background: #fff;
+ transition: all 0.3s;
+ }
+
+ .template-item:hover {
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ }
+
+ .template-info h4 {
+ margin: 0 0 0.5rem 0;
+ color: #333;
+ font-size: 1.1rem;
+ }
+
+ .template-info p {
+ margin: 0 0 0.25rem 0;
+ color: #666;
+ }
+
+ .template-info small {
+ color: #999;
+ font-size: 0.85rem;
+ }
+
+ .template-actions {
+ display: flex;
+ gap: 0.5rem;
+ }
+
+ .btn-sm {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ }
+ `;
+
// ============ 渲染 ============
return (
-
+ <>
+
+
🎮 主播控制台
@@ -295,6 +507,143 @@ const StreamerConsole = () => {
{error &&
{error}
}
+ {/* 宝箱模板管理卡片 */}
+
+
+
+ 最多可创建3个模板
+
+
+ {showTemplateForm && (
+
+ )}
+
+ {/* 模板列表 */}
+ {templates.length === 0 ? (
+ 暂无模板,点击上方按钮创建
+ ) : (
+
+ {templates.map((template) => (
+
+
+
{template.name}
+
{template.title}
+
+ {template.option_a} vs {template.option_b} | {template.countdown_seconds}秒
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
{/* 全局提醒:如果有封盘状态的宝箱 */}
{lockedChests.length > 0 && (
@@ -320,7 +669,12 @@ const StreamerConsole = () => {
)}
{showCreateForm && (
-
+
+ {selectedTemplateId && (
+
+ ✓ 已从模板加载配置,您可以直接创建或修改后创建
+
+ )}
)}
-
+
+ >
);
};
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index b59cdc9..8ed3e1c 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -1,5 +1,5 @@
import axios from 'axios';
-import type { User, LoginResponse, Transaction, Chest, ChestCreate, Bet, BetCreate, ChestSettle, SystemConfig, StreamerProfile, StreamerStatistics, Announcement } from '../types';
+import type { User, LoginResponse, Transaction, Chest, ChestCreate, Bet, BetCreate, ChestSettle, SystemConfig, StreamerProfile, StreamerStatistics, Announcement, ChestTemplate, ChestTemplateCreate, ChestTemplateUpdate } from '../types';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
@@ -174,6 +174,27 @@ export const gameApi = {
const response = await api.get
(`/api/chests/${chestId}/bets`);
return response.data;
},
+
+ // 宝箱模板相关API
+ getTemplates: async (): Promise => {
+ const response = await api.get('/api/templates');
+ return response.data;
+ },
+
+ createTemplate: async (data: ChestTemplateCreate): Promise => {
+ const response = await api.post('/api/templates', data);
+ return response.data;
+ },
+
+ updateTemplate: async (templateId: number, data: ChestTemplateUpdate): Promise => {
+ const response = await api.put(`/api/templates/${templateId}`, data);
+ return response.data;
+ },
+
+ deleteTemplate: async (templateId: number): Promise<{ message: string }> => {
+ const response = await api.delete(`/api/templates/${templateId}`);
+ return response.data;
+ },
};
// 系统配置相关
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index ba4999d..1880aa4 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -177,4 +177,35 @@ export interface StreamerStatistics {
total_commission: number;
average_chest_value: number;
success_rate: number;
+}
+
+// 宝箱模板
+export interface ChestTemplate {
+ id: number;
+ streamer_id: number;
+ name: string;
+ title: string;
+ option_a: string;
+ option_b: string;
+ countdown_seconds: number;
+ created_at: string;
+ updated_at: string;
+}
+
+// 创建宝箱模板
+export interface ChestTemplateCreate {
+ name: string;
+ title: string;
+ option_a: string;
+ option_b: string;
+ countdown_seconds: number;
+}
+
+// 更新宝箱模板
+export interface ChestTemplateUpdate {
+ name?: string;
+ title?: string;
+ option_a?: string;
+ option_b?: string;
+ countdown_seconds?: number;
}
\ No newline at end of file