🎯 重构登录逻辑:以人为核心的三种出战模式

核心变更:
- 单枪匹马 (solo) - 人类单独出战
- 组队团战 (team) - 人类 +N 龙虾
- 独当一面 (agent_only) - 龙虾单独出征

后端:
- users/views.py: 支持多选 agent_ids
- 新增 mode_names 映射
- 错误提示优化

前端:
- 新模式选择 UI(带图标和说明)
- 多选龙虾复选框
- 实时显示已选龙虾数量
- 选中模式高亮显示

测试:
- test_new_login.py: 完整测试三种模式
- 绑定第二只龙虾(龙虾监控 🦞)

结果:
 单枪匹马 - 1 个人类座位
 组队团战 - 1+N 个座位(人类 + 龙虾)
 独当一面 - N 个龙虾座位
This commit is contained in:
2026-04-04 16:41:13 +08:00
parent 97e4a6fecb
commit 65000664ef
3 changed files with 191 additions and 66 deletions

View File

@@ -14,9 +14,9 @@ axios.interceptors.request.use(config => {
function LoginPage() {
const [username, setUsername] = useState('test');
const [password, setPassword] = useState('test123');
const [loginMode, setLoginMode] = useState('human_only');
const [mode, setMode] = useState('solo');
const [agents, setAgents] = useState([]);
const [selectedAgent, setSelectedAgent] = useState('');
const [selectedAgents, setSelectedAgents] = useState([]);
const navigate = useNavigate();
// 扫描本机龙虾
@@ -28,9 +28,6 @@ function LoginPage() {
try {
const res = await axios.get(`${API_BASE}/user/scan-local-agents/`);
setAgents(res.data.agents || []);
if (res.data.agents?.length > 0) {
setSelectedAgent(res.data.agents[0].agent_id);
}
} catch (error) {
console.error('扫描龙虾失败:', error);
}
@@ -42,24 +39,32 @@ function LoginPage() {
const payload = {
username,
password,
login_mode: loginMode
mode
};
if (loginMode !== 'human_only' && selectedAgent) {
payload.selected_agent_id = selectedAgent;
if (mode !== 'solo' && selectedAgents.length > 0) {
payload.agent_ids = selectedAgents;
}
const res = await axios.post(`${API_BASE}/auth/login/`, payload);
localStorage.setItem('token', res.data.token);
localStorage.setItem('user', JSON.stringify(res.data.user));
localStorage.setItem('sessions', JSON.stringify(res.data.sessions));
localStorage.setItem('login_mode', res.data.login_mode);
localStorage.setItem('mode', res.data.mode);
navigate('/meetings');
} catch (error) {
alert('登录失败:' + (error.response?.data?.detail || error.response?.data?.error || error.message));
}
};
const toggleAgent = (agentId) => {
setSelectedAgents(prev =>
prev.includes(agentId)
? prev.filter(id => id !== agentId)
: [...prev, agentId]
);
};
return (
<div style={styles.center}>
<div style={styles.card}>
@@ -68,64 +73,71 @@ function LoginPage() {
<input type="text" placeholder="用户名" value={username} onChange={e => setUsername(e.target.value)} style={styles.input} required />
<input type="password" placeholder="密码" value={password} onChange={e => setPassword(e.target.value)} style={styles.input} required />
{/* 身份模式选择 */}
{/* 出战模式选择 */}
<div style={{margin: '15px 0'}}>
<label style={{display: 'block', marginBottom: '10px', fontWeight: '600'}}>登录身份</label>
<label style={{display: 'block', marginBottom: '8px', cursor: 'pointer'}}>
<label style={{display: 'block', marginBottom: '10px', fontWeight: '600'}}>🎯 出战模式</label>
<label style={{display: 'block', marginBottom: '12px', cursor: 'pointer', padding: '10px', background: mode === 'solo' ? '#e7f3ff' : 'white', borderRadius: '8px', border: '1px solid #2196f3'}}>
<input
type="radio"
name="loginMode"
value="human_only"
checked={loginMode === 'human_only'}
onChange={e => setLoginMode(e.target.value)}
name="mode"
value="solo"
checked={mode === 'solo'}
onChange={e => setMode(e.target.value)}
/>
{' '}👤 人类身份纯人类参会
{' '}🥷 <strong>单枪匹马</strong>
</label>
<label style={{display: 'block', marginBottom: '8px', cursor: 'pointer'}}>
<label style={{display: 'block', marginBottom: '12px', cursor: 'pointer', padding: '10px', background: mode === 'team' ? '#e7f3ff' : 'white', borderRadius: '8px', border: '1px solid #2196f3'}}>
<input
type="radio"
name="loginMode"
name="mode"
value="team"
checked={mode === 'team'}
onChange={e => setMode(e.target.value)}
/>
{' '}🛡 <strong>组队团战</strong> + N
</label>
<label style={{display: 'block', marginBottom: '12px', cursor: 'pointer', padding: '10px', background: mode === 'agent_only' ? '#e7f3ff' : 'white', borderRadius: '8px', border: '1px solid #2196f3'}}>
<input
type="radio"
name="mode"
value="agent_only"
checked={loginMode === 'agent_only'}
onChange={e => setLoginMode(e.target.value)}
checked={mode === 'agent_only'}
onChange={e => setMode(e.target.value)}
/>
{' '}🦞 龙虾身份Agent 参会
</label>
<label style={{display: 'block', marginBottom: '8px', cursor: 'pointer'}}>
<input
type="radio"
name="loginMode"
value="both"
checked={loginMode === 'both'}
onChange={e => setLoginMode(e.target.value)}
/>
{' '}👤+ 双重身份人类 + 龙虾
{' '} <strong>独当一面</strong>
</label>
</div>
{/* 龙虾选择 */}
{loginMode !== 'human_only' && (
<div style={{margin: '15px 0'}}>
<label style={{display: 'block', marginBottom: '8px', fontWeight: '600'}}>选择龙虾</label>
<select
value={selectedAgent}
onChange={e => setSelectedAgent(e.target.value)}
style={styles.input}
>
{agents.length === 0 ? (
<option value="">未找到龙虾</option>
) : (
agents.map(a => (
<option key={a.agent_id} value={a.agent_id}>
{a.agent_id} ({a.instance_name})
</option>
))
)}
</select>
{/* 龙虾选择(组队或独当一面) */}
{mode !== 'solo' && (
<div style={{margin: '15px 0', padding: '15px', background: '#f9f9f9', borderRadius: '8px'}}>
<label style={{display: 'block', marginBottom: '10px', fontWeight: '600'}}>🦐 选择龙虾队友</label>
{agents.length === 0 ? (
<p style={{color: '#999', fontSize: '14px'}}>未找到可用龙虾</p>
) : (
agents.map(a => (
<label key={a.agent_id} style={{display: 'flex', alignItems: 'center', marginBottom: '8px', cursor: 'pointer'}}>
<input
type="checkbox"
checked={selectedAgents.includes(a.agent_id)}
onChange={() => toggleAgent(a.agent_id)}
style={{marginRight: '10px'}}
/>
<span style={{fontSize: '16px', marginRight: '8px'}}>{a.agent_emoji || '🤖'}</span>
<span>{a.agent_id}</span>
<span style={{color: '#999', fontSize: '12px', marginLeft: '8px'}}>({a.instance_name})</span>
</label>
))
)}
{selectedAgents.length > 0 && (
<p style={{marginTop: '10px', color: '#2196f3', fontWeight: '600'}}>
已选 {selectedAgents.length} 只龙虾队友 🦸
</p>
)}
</div>
)}
<button type="submit" style={styles.btn}>登录</button>
<button type="submit" style={styles.btn}>🚀 登录出征</button>
</form>
</div>
</div>