feat: add auth api for aura
parent
1e55de78dd
commit
6dc5f1f750
|
|
@ -54,9 +54,10 @@ export interface Agent {
|
|||
}
|
||||
|
||||
export const AgentAPI = {
|
||||
list: () => api.get<Agent[]>('/agents').then((r) => r.data),
|
||||
list: (phone: string) => api.post<Agent[]>('/agents', { phone }).then((r) => r.data),
|
||||
mine: (phone: string) => api.post<Agent[]>('/agents/mine', { phone }).then((r) => r.data),
|
||||
detail: (id: string) => api.get<Agent>(`/agents/${id}`).then((r) => r.data),
|
||||
create: (payload: Partial<Agent>) => api.post<Agent>('/agents', payload).then((r) => r.data),
|
||||
create: (payload: Partial<Agent>) => api.post<Agent>('/agents/create', payload).then((r) => r.data),
|
||||
update: (id: string, payload: Partial<Agent>) => api.put<Agent>(`/agents/${id}`, payload).then((r) => r.data),
|
||||
remove: (id: string) => api.delete(`/agents/${id}`).then((r) => r.data),
|
||||
|
||||
|
|
|
|||
|
|
@ -3,28 +3,28 @@ import { api, API_BASE_URL } from './http';
|
|||
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
name: string;
|
||||
role: 'admin' | 'user';
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const AuthAPI = {
|
||||
me: () => api.get<AuthUser>('/auth/me').then((r) => r.data),
|
||||
verify: async (email: string, password: string) => {
|
||||
verify: async (phone: string, password: string) => {
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE_URL}/urser`, { email, password }, { timeout: 3000 });
|
||||
const res = await axios.post(`${API_BASE_URL}/urser`, { phone, password }, { timeout: 3000 });
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
console.warn('Backend /urser not available, fallback to mock true', e);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
login: (email: string, password: string) => api.post<AuthUser>('/auth/login', { email, password }).then((r) => r.data),
|
||||
register: (payload: { email: string; password: string; name: string; inviteCode?: string }) =>
|
||||
login: (phone: string, password: string) => api.post<AuthUser>('/auth/login', { phone, password }).then((r) => r.data),
|
||||
register: (payload: { phone: string; password: string; name: string; inviteCode?: string }) =>
|
||||
api.post<AuthUser>('/auth/register', payload).then((r) => r.data),
|
||||
logout: () => api.post('/auth/logout').then((r) => r.data),
|
||||
listInvites: () => api.get('/auth/invites').then((r) => r.data),
|
||||
createInvite: (payload: { email?: string; teamId?: string; role?: string; ttlHours?: number }) => api.post('/auth/invites', payload).then((r) => r.data),
|
||||
createInvite: (payload: { phone?: string; teamId?: string; role?: string; ttlHours?: number }) => api.post('/auth/invites', payload).then((r) => r.data),
|
||||
deleteInvite: (code: string) => api.delete(`/auth/invites/${code}`).then((r) => r.data)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import axios from 'axios';
|
||||
import { clearUserStorage } from '../utils/storage';
|
||||
|
||||
export const API_BASE_URL = 'https://tianchaoai.cc/aura/v1';
|
||||
export const API_BASE_URL = import.meta.env.DEV ? '/api' : 'https://tianchaoai.cc/aura/v1';
|
||||
const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
||||
export const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
export const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const isMockAuth = () => typeof localStorage !== 'undefined' && localStorage.getItem('mock-auth') === '1';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
|
|
@ -12,15 +12,36 @@ export const api = axios.create({
|
|||
withCredentials: true
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('aura-token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(r) => r,
|
||||
(err) => {
|
||||
const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login');
|
||||
if (err?.response?.status === 401 && !isLoginPage && !isMockAuth()) {
|
||||
const next = encodeURIComponent(location.pathname + location.search);
|
||||
const isAuthPath = err.config?.url?.includes('/auth/login') || err.config?.url?.includes('/auth/register');
|
||||
|
||||
if (err?.response?.status === 401 && !isLoginPage && !isAuthPath) {
|
||||
clearUserStorage();
|
||||
|
||||
let nextPath = location.pathname;
|
||||
let nextSearch = location.search;
|
||||
|
||||
// 如果是聊天页面,只进入 /chat 列表页,不进入具体的智能体或会话,防止切换账号后的权限冲突
|
||||
if (nextPath.includes('/chat')) {
|
||||
nextPath = '/chat';
|
||||
nextSearch = '';
|
||||
}
|
||||
|
||||
const next = encodeURIComponent(nextPath + nextSearch);
|
||||
location.href = `${withAppBase('/login')}?next=${next}`;
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export default function Sidebar({ onOpenPalette, onNavigate }: Props) {
|
|||
items: [
|
||||
{
|
||||
key: 'name',
|
||||
label: <span className="sidebar-user-role">{user.email}</span>,
|
||||
label: <span className="sidebar-user-role">{user.phone}</span>,
|
||||
disabled: true
|
||||
},
|
||||
{ type: 'divider' },
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Agent, AgentAPI } from '../../api';
|
||||
import { useAuth } from '../../store/auth';
|
||||
|
||||
export function useAgentListLogic() {
|
||||
const { user } = useAuth();
|
||||
const [list, setList] = useState<Agent[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
if (!user?.phone) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
setList(await AgentAPI.list());
|
||||
setList(await AgentAPI.mine(user.phone));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -16,7 +19,7 @@ export function useAgentListLogic() {
|
|||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
}, [user?.phone]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await AgentAPI.remove(id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
.agent-list-header {
|
||||
border-radius: 24px;
|
||||
padding: 30px 30px 26px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 48%, rgba(239,246,255,0.96) 100%);
|
||||
border: 1px solid rgba(8, 145, 178, 0.12);
|
||||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.06);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.agent-list-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.agent-list-intro {
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.agent-list-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.78);
|
||||
border: 1px solid rgba(8, 145, 178, 0.10);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agent-list-badge-icon {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.agent-list-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.agent-list-stat-card {
|
||||
border-radius: 18px;
|
||||
padding: 16px 18px;
|
||||
background: rgba(255,255,255,0.72);
|
||||
border: 1px solid rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.agent-list-stat-label {
|
||||
font-size: 12.5px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.agent-list-stat-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-list-stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.agent-list-stat-badge {
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
min-height: 292px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%);
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.045);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-card-avatar {
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.agent-card-title-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-card-name {
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-card-update-time {
|
||||
font-size: 12.5px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.agent-card-desc-container {
|
||||
margin-top: 16px;
|
||||
padding: 16px 16px 18px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%);
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.agent-card-desc {
|
||||
min-height: 66px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.7;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-card-tags {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* 修复模型标签上下不居中问题 */
|
||||
.agent-card-tag-model {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.agent-card-tag-model-text {
|
||||
display: inline-block;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2; /* 确保文字行高不影响居中 */
|
||||
}
|
||||
|
||||
.agent-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.agent-list-banner {
|
||||
margin-top: 24px;
|
||||
border-radius: 20px;
|
||||
padding: 18px 20px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-list-banner-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.agent-list-banner-desc {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
|
@ -9,8 +9,8 @@ import {
|
|||
import { Button, Col, Row, Empty, Popconfirm, App as AntApp, Tag, Space } from 'antd';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Agent } from '../../../api';
|
||||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import './AgentListWeb.css';
|
||||
|
||||
interface Props {
|
||||
logic: AgentListLogicOutput;
|
||||
|
|
@ -23,44 +23,11 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 24,
|
||||
padding: '30px 30px 26px',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 48%, rgba(239,246,255,0.96) 100%)',
|
||||
border: '1px solid rgba(8, 145, 178, 0.12)',
|
||||
boxShadow: '0 20px 48px rgba(15, 23, 42, 0.06)',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
gap: 20,
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 22,
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 620 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '6px 12px',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(255,255,255,0.78)',
|
||||
border: '1px solid rgba(8, 145, 178, 0.10)',
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<RobotOutlined style={{ color: 'var(--color-brand)' }} />
|
||||
<div className="agent-list-header">
|
||||
<div className="agent-list-header-content">
|
||||
<div className="agent-list-intro">
|
||||
<div className="agent-list-badge">
|
||||
<RobotOutlined className="agent-list-badge-icon" />
|
||||
我的 Agent 资产
|
||||
</div>
|
||||
|
||||
|
|
@ -82,28 +49,17 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 14 }}>
|
||||
<div className="agent-list-stats-grid">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
style={{
|
||||
borderRadius: 18,
|
||||
padding: '16px 18px',
|
||||
background: 'rgba(255,255,255,0.72)',
|
||||
border: '1px solid rgba(255,255,255,0.7)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 10 }}>{item.label}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||||
<span style={{ fontSize: 28, fontWeight: 700, color: 'var(--color-text)' }}>{item.value}</span>
|
||||
<div key={item.label} className="agent-list-stat-card">
|
||||
<div className="agent-list-stat-label">{item.label}</div>
|
||||
<div className="agent-list-stat-content">
|
||||
<span className="agent-list-stat-value">{item.value}</span>
|
||||
<span
|
||||
className="agent-list-stat-badge"
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
padding: '4px 8px',
|
||||
background: item.tone,
|
||||
color: item.color,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
实时统计
|
||||
|
|
@ -126,20 +82,11 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
<Row gutter={[18, 18]}>
|
||||
{list.map((a) => (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={a.id}>
|
||||
<div
|
||||
className="agent-card"
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
minHeight: 292,
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%)',
|
||||
boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div className="agent-card">
|
||||
<div className="agent-card-header">
|
||||
<div
|
||||
className="avatar"
|
||||
style={{ background: a.avatar || 'var(--gradient-brand)', borderRadius: '50%', overflow: 'hidden', width: 54, height: 54 }}
|
||||
className="agent-card-avatar"
|
||||
style={{ background: a.avatar || 'var(--gradient-brand)' }}
|
||||
>
|
||||
{isImageUrl(a.avatar) ? (
|
||||
<img src={a.avatar} className="w-full h-full object-cover" alt="avatar" />
|
||||
|
|
@ -147,29 +94,21 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
(a.name?.charAt(0) || '?').toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 17, color: 'var(--color-text)', marginBottom: 4 }}>{a.name}</div>
|
||||
<div style={{ fontSize: 12.5, color: 'var(--color-text-tertiary)' }}>
|
||||
<div className="agent-card-title-group">
|
||||
<div className="agent-card-name">{a.name}</div>
|
||||
<div className="agent-card-update-time">
|
||||
最近更新于 {dayjs(a.updated_at).format('YYYY-MM-DD')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: '16px 16px 18px',
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%)',
|
||||
border: '1px solid rgba(148, 163, 184, 0.14)',
|
||||
}}
|
||||
>
|
||||
<div className="desc" style={{ minHeight: 66, fontSize: 13.5, lineHeight: 1.7 }}>
|
||||
<div className="agent-card-desc-container">
|
||||
<div className="agent-card-desc">
|
||||
{a.description || '还没有填写描述,可以补充这个智能体适合解决什么问题。'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space size={6} wrap style={{ marginTop: 14 }}>
|
||||
<Space size={6} wrap className="agent-card-tags">
|
||||
{a.visibility === 'public' && (
|
||||
<Tag bordered={false} style={{ background: 'var(--color-success-soft)', color: 'var(--color-success)', borderRadius: 999, margin: 0 }}>
|
||||
公开
|
||||
|
|
@ -186,8 +125,12 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
</Tag>
|
||||
)}
|
||||
{getModelLabel(a.model) && (
|
||||
<Tag bordered={false} style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, maxWidth: '100%' }}>
|
||||
<span style={{ display: 'inline-block', maxWidth: 190, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
<Tag
|
||||
bordered={false}
|
||||
className="agent-card-tag-model"
|
||||
style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, maxWidth: '100%' }}
|
||||
>
|
||||
<span className="agent-card-tag-model-text">
|
||||
{getModelLabel(a.model)}
|
||||
</span>
|
||||
</Tag>
|
||||
|
|
@ -199,7 +142,7 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
)}
|
||||
</Space>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 'auto', paddingTop: 16, borderTop: '1px solid var(--color-border)' }}>
|
||||
<div className="agent-card-actions">
|
||||
<Link to={`/chat/${a.id}`} style={{ flex: 1 }}>
|
||||
<Button type="primary" block icon={<MessageOutlined />} style={{ borderRadius: 12, height: 40, fontWeight: 600 }}>
|
||||
聊天
|
||||
|
|
@ -230,25 +173,12 @@ export default function AgentListWeb({ logic }: Props) {
|
|||
)}
|
||||
|
||||
{list.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 24,
|
||||
borderRadius: 20,
|
||||
padding: '18px 20px',
|
||||
background: 'var(--color-surface)',
|
||||
border: '1px solid var(--color-border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<div className="agent-list-banner">
|
||||
<div>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)', marginBottom: 4 }}>
|
||||
<div className="agent-list-banner-title">
|
||||
想创建新的智能体入口?
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>
|
||||
<div className="agent-list-banner-desc">
|
||||
统一从智能体广场进入,保证创建流程和发现体验保持一致。
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function useLoginPageLogic() {
|
|||
const onLogin = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(values.email, values.password);
|
||||
await login(values.phone, values.password);
|
||||
message.success('登录成功');
|
||||
navigate(next, { replace: true });
|
||||
} catch (e: any) {
|
||||
|
|
@ -31,13 +31,14 @@ export function useLoginPageLogic() {
|
|||
setLoading(true);
|
||||
try {
|
||||
await register({
|
||||
email: values.email,
|
||||
phone: values.phone,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
inviteCode: values.inviteCode || undefined
|
||||
});
|
||||
message.success('注册成功,已自动登录');
|
||||
navigate(next, { replace: true });
|
||||
message.success('注册成功,请登录');
|
||||
setTab('login');
|
||||
return { phone: values.phone, password: values.password };
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.error ?? e?.message ?? '注册失败');
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,26 @@
|
|||
import { Alert, Button, Form, Input, Tabs } from 'antd';
|
||||
import { Button, Form, Input, Tabs } from 'antd';
|
||||
import type { LoginPageLogic, LoginTab } from '../LoginPageLogic';
|
||||
|
||||
export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) {
|
||||
const { tab, setTab, loading, onLogin, onRegister } = logic;
|
||||
const [loginForm] = Form.useForm();
|
||||
const [registerForm] = Form.useForm();
|
||||
|
||||
const handleRegister = async (values: any) => {
|
||||
const result = await onRegister(values);
|
||||
if (result) {
|
||||
loginForm.setFieldsValue({
|
||||
phone: result.phone,
|
||||
password: result.password,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-card">
|
||||
<div className="login-card-header">
|
||||
<h2 className="login-card-title">欢迎回来</h2>
|
||||
<div className="login-card-subtitle">使用邮箱登录或注册以继续</div>
|
||||
<div className="login-card-subtitle">使用手机号登录或注册以继续</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
|
|
@ -19,9 +31,14 @@ export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) {
|
|||
key: 'login',
|
||||
label: '登录',
|
||||
children: (
|
||||
<Form layout="vertical" onFinish={onLogin} className="login-form">
|
||||
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email', message: '请填写合法邮箱' }]}>
|
||||
<Input placeholder="you@example.com" size="large" autoFocus />
|
||||
<Form
|
||||
form={loginForm}
|
||||
layout="vertical"
|
||||
onFinish={onLogin}
|
||||
className="login-form"
|
||||
>
|
||||
<Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请填写手机号' }]}>
|
||||
<Input placeholder="13800138000" size="large" autoFocus />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="密码" rules={[{ required: true }]}>
|
||||
<Input.Password placeholder="••••••" size="large" />
|
||||
|
|
@ -36,15 +53,14 @@ export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) {
|
|||
key: 'register',
|
||||
label: '注册',
|
||||
children: (
|
||||
<Form layout="vertical" onFinish={onRegister} className="login-form">
|
||||
<Alert
|
||||
className="login-register-alert"
|
||||
type="info"
|
||||
showIcon
|
||||
message="第一个注册的用户自动成为管理员;之后需要邀请码"
|
||||
/>
|
||||
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input placeholder="you@example.com" size="large" />
|
||||
<Form
|
||||
form={registerForm}
|
||||
layout="vertical"
|
||||
onFinish={handleRegister}
|
||||
className="login-form"
|
||||
>
|
||||
<Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请填写手机号' }]}>
|
||||
<Input placeholder="13800138000" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="昵称" rules={[{ required: true }]}>
|
||||
<Input placeholder="张三" size="large" />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function useTeamsPageLogic() {
|
|||
if (!active) return;
|
||||
const inv = await AuthAPI.createInvite({
|
||||
teamId: active.id,
|
||||
email: v.email || undefined,
|
||||
phone: v.phone || undefined,
|
||||
ttlHours: Number(v.ttlHours) || 168,
|
||||
});
|
||||
setLastInviteCode(inv.code);
|
||||
|
|
|
|||
|
|
@ -145,8 +145,6 @@ export default function ChatInput(props: {
|
|||
a.name.toLowerCase().includes(mentionQuery.toLowerCase())
|
||||
);
|
||||
|
||||
console.log('[@mention] filtered:', { mentionQuery, count: filteredAgents.length, agents: filteredAgents.map(a => a.name) });
|
||||
|
||||
const handleSelectAgent = (agent: Agent) => {
|
||||
const textarea = inputRef.current?.resizableTextArea?.textArea;
|
||||
if (!textarea) return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { Agent, BranchInfo, ChatMessage, ModelOverrides } from '../../../api';
|
||||
import { AgentAPI, ChatAPI } from '../../../api';
|
||||
import { useAuth } from '../../../store/auth';
|
||||
import { parseAgentModels } from '../utils/agentModels';
|
||||
|
||||
export function useChatData(args: {
|
||||
|
|
@ -13,6 +14,7 @@ export function useChatData(args: {
|
|||
setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void;
|
||||
abort: () => void;
|
||||
}) {
|
||||
const { user } = useAuth();
|
||||
const { agentId, roomId, highlightId, setHighlightId, scrollBottom, initialScrollDoneRef, setOverrides, abort } = args;
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [agentList, setAgentList] = useState<Agent[]>([]);
|
||||
|
|
@ -40,8 +42,9 @@ export function useChatData(args: {
|
|||
};
|
||||
|
||||
const loadAgentList = async () => {
|
||||
if (!user?.phone) return;
|
||||
try {
|
||||
const list = await AgentAPI.list();
|
||||
const list = await AgentAPI.list(user.phone);
|
||||
setAgentList(list);
|
||||
} catch {
|
||||
// ignore
|
||||
|
|
@ -69,7 +72,7 @@ export function useChatData(args: {
|
|||
|
||||
useEffect(() => {
|
||||
loadAgentList();
|
||||
}, []);
|
||||
}, [user?.phone]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentId) {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { create } from 'zustand';
|
||||
import { AuthAPI, AuthUser } from '../api';
|
||||
import { clearUserStorage } from '../utils/storage';
|
||||
|
||||
interface AuthState {
|
||||
user: AuthUser | null;
|
||||
loading: boolean;
|
||||
/** 启动时调用:从后端拉当前登录态 */
|
||||
bootstrap: () => Promise<void>;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (p: { email: string; password: string; name: string; inviteCode?: string }) => Promise<void>;
|
||||
login: (phone: string, password: string) => Promise<void>;
|
||||
register: (p: { phone: string; password: string; name: string; inviteCode?: string }) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const loadSavedUser = (): AuthUser | null => {
|
||||
try {
|
||||
const raw = localStorage.getItem('mock-user');
|
||||
const raw = localStorage.getItem('aura-user');
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as AuthUser;
|
||||
} catch {
|
||||
|
|
@ -25,38 +26,28 @@ export const useAuth = create<AuthState>((set) => ({
|
|||
user: typeof localStorage === 'undefined' ? null : loadSavedUser(),
|
||||
loading: false,
|
||||
bootstrap: async () => {
|
||||
set({ loading: false });
|
||||
},
|
||||
login: async (email, password) => {
|
||||
const ok = await AuthAPI.verify(email, password);
|
||||
if (!ok) throw new Error('身份验证失败');
|
||||
const u: AuthUser = {
|
||||
id: 'mock',
|
||||
email,
|
||||
name: email.split('@')[0] || 'User',
|
||||
role: 'user'
|
||||
};
|
||||
try {
|
||||
localStorage.setItem('mock-auth', '1');
|
||||
localStorage.setItem('mock-user', JSON.stringify(u));
|
||||
} catch {
|
||||
}
|
||||
const u = await AuthAPI.me();
|
||||
set({ user: u });
|
||||
localStorage.setItem('aura-user', JSON.stringify(u));
|
||||
} catch {}
|
||||
},
|
||||
login: async (phone, password) => {
|
||||
const u = await AuthAPI.login(phone, password);
|
||||
if (!u || !u.token) throw new Error('登录失败');
|
||||
localStorage.setItem('aura-token', u.token);
|
||||
localStorage.setItem('aura-user', JSON.stringify(u));
|
||||
set({ user: u });
|
||||
},
|
||||
register: async (p) => {
|
||||
const u = await AuthAPI.register(p);
|
||||
set({ user: u });
|
||||
// 注册成功后不自动登录
|
||||
await AuthAPI.register(p);
|
||||
},
|
||||
logout: async () => {
|
||||
try {
|
||||
await AuthAPI.logout();
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem('mock-auth');
|
||||
localStorage.removeItem('mock-user');
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
clearUserStorage();
|
||||
set({ user: null });
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 清除用户相关的本地存储
|
||||
* 包括 token、用户信息以及聊天相关的状态(如上次访问的房间号)
|
||||
*/
|
||||
export function clearUserStorage() {
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
// 清除 aura 前缀的 key (aura-token, aura-user)
|
||||
// 以及聊天相关的 key (chat:lastRoom:*)
|
||||
if (key.startsWith('aura-') || key.startsWith('chat:')) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
}
|
||||
Loading…
Reference in New Issue