主播后台添加模板页面
This commit is contained in:
parent
4927420266
commit
e72df24934
@ -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="更新时间")
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -80,3 +80,39 @@ class ChestSettle(BaseModel):
|
||||
class ChestLock(BaseModel):
|
||||
"""宝箱封盘"""
|
||||
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
|
||||
@ -615,3 +615,104 @@ class GameService:
|
||||
获取宝箱下注记录
|
||||
"""
|
||||
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
|
||||
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@ -5,7 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>互动竞猜开宝箱系统</title>
|
||||
<script type="module" crossorigin src="/assets/index-ByW5lIiv.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DDg-nT0I.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9Z2CB14.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -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<ChestTemplate[]>([]);
|
||||
const [showTemplateForm, setShowTemplateForm] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<ChestTemplate | null>(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<number | null>(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,8 +414,67 @@ const StreamerConsole = () => {
|
||||
return <Loading text="加载控制台中..." />;
|
||||
}
|
||||
|
||||
// ============ 样式 ============
|
||||
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 (
|
||||
<>
|
||||
<style>{templateStyles}</style>
|
||||
<div className="streamer-console">
|
||||
<div className="console-header">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
||||
@ -295,6 +507,143 @@ const StreamerConsole = () => {
|
||||
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
|
||||
{/* 宝箱模板管理卡片 */}
|
||||
<Card title="📋 宝箱模板管理" className="mb-3">
|
||||
<div className="mb-3">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowTemplateForm(!showTemplateForm)}
|
||||
>
|
||||
{showTemplateForm ? '取消' : '+ 创建模板'}
|
||||
</button>
|
||||
<span className="ml-2 text-secondary">最多可创建3个模板</span>
|
||||
</div>
|
||||
|
||||
{showTemplateForm && (
|
||||
<form onSubmit={handleSaveTemplate} className="template-form mb-3">
|
||||
<div className="form-group">
|
||||
<label className="form-label">模板名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
placeholder="例如: 游戏类宝箱"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">宝箱标题</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={templateTitle}
|
||||
onChange={(e) => setTemplateTitle(e.target.value)}
|
||||
placeholder="例如: 这把能吃鸡吗?"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="form-label">选项A</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={templateOptionA}
|
||||
onChange={(e) => setTemplateOptionA(e.target.value)}
|
||||
placeholder="例如: 能"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">选项B</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={templateOptionB}
|
||||
onChange={(e) => setTemplateOptionB(e.target.value)}
|
||||
placeholder="例如: 不能"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">下注时长(秒)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={templateCountdown}
|
||||
onChange={(e) => setTemplateCountdown(parseInt(e.target.value))}
|
||||
min="60"
|
||||
max="3600"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={resetTemplateForm}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={templateLoading}
|
||||
>
|
||||
{templateLoading ? '保存中...' : (editingTemplate ? '更新模板' : '保存模板')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* 模板列表 */}
|
||||
{templates.length === 0 ? (
|
||||
<p className="text-center text-secondary">暂无模板,点击上方按钮创建</p>
|
||||
) : (
|
||||
<div className="templates-list">
|
||||
{templates.map((template) => (
|
||||
<div key={template.id} className="template-item">
|
||||
<div className="template-info">
|
||||
<h4>{template.name}</h4>
|
||||
<p>{template.title}</p>
|
||||
<small>
|
||||
{template.option_a} vs {template.option_b} | {template.countdown_seconds}秒
|
||||
</small>
|
||||
</div>
|
||||
<div className="template-actions">
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
onClick={() => handleUseTemplate(template)}
|
||||
>
|
||||
使用
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleEditTemplate(template)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleDeleteTemplate(template.id)}
|
||||
disabled={templateLoading}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 全局提醒:如果有封盘状态的宝箱 */}
|
||||
{lockedChests.length > 0 && (
|
||||
<div className="locked-chests-alert">
|
||||
@ -320,7 +669,12 @@ const StreamerConsole = () => {
|
||||
)}
|
||||
|
||||
{showCreateForm && (
|
||||
<Card title="创建新宝箱" className="mb-3">
|
||||
<Card title="创建新宝箱" className="mb-3 create-form">
|
||||
{selectedTemplateId && (
|
||||
<div className="alert alert-info mb-3">
|
||||
✓ 已从模板加载配置,您可以直接创建或修改后创建
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleCreateChest}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">标题</label>
|
||||
@ -548,6 +902,7 @@ const StreamerConsole = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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<Bet[]>(`/api/chests/${chestId}/bets`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 宝箱模板相关API
|
||||
getTemplates: async (): Promise<ChestTemplate[]> => {
|
||||
const response = await api.get<ChestTemplate[]>('/api/templates');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createTemplate: async (data: ChestTemplateCreate): Promise<ChestTemplate> => {
|
||||
const response = await api.post<ChestTemplate>('/api/templates', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateTemplate: async (templateId: number, data: ChestTemplateUpdate): Promise<ChestTemplate> => {
|
||||
const response = await api.put<ChestTemplate>(`/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;
|
||||
},
|
||||
};
|
||||
|
||||
// 系统配置相关
|
||||
|
||||
@ -178,3 +178,34 @@ export interface StreamerStatistics {
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user