From 2a4dc720599f3df9dd16d0ce9bcb21ccdced0453 Mon Sep 17 00:00:00 2001 From: yannyang Date: Mon, 27 Jul 2026 00:42:15 +0800 Subject: [PATCH] feat: support membership and sub account --- src/App.tsx | 13 +- src/api/http.ts | 2 +- src/api/index.ts | 1 + src/api/membership.ts | 111 ++++++ src/components/Icon/index.tsx | 62 ++++ src/components/Sidebar.css | 252 +++++++++++++ src/components/Sidebar.tsx | 167 ++++----- src/main.tsx | 4 +- .../components/MarketplacePageWeb.tsx | 42 +-- .../styles/marketplace-page-web.css | 37 +- src/pages/Pricing/PricingLogic.ts | 165 +++++++++ src/pages/Pricing/components/PricingH5.tsx | 136 +++++++ src/pages/Pricing/components/PricingWeb.tsx | 136 +++++++ src/pages/Pricing/styles/pricing.css | 215 +++++++++++ src/pages/PricingPage.tsx | 24 ++ src/pages/Profile/ProfileLogic.ts | 138 +++++++ src/pages/Profile/components/ProfileH5.tsx | 175 +++++++++ src/pages/Profile/components/ProfileWeb.tsx | 257 ++++++++++++++ src/pages/Profile/styles/profile.css | 145 ++++++++ src/pages/ProfilePage.tsx | 24 ++ .../components/StatsTopAgentsCard.tsx | 2 +- src/pages/chat/components/AgentSidebar.css | 110 +++++- src/pages/chat/components/AgentSidebar.tsx | 111 ++++-- src/pages/chat/components/ChatBody.css | 86 +++++ src/pages/chat/components/ChatBody.tsx | 99 ++---- src/pages/chat/components/ChatHeader.css | 114 ++++++ src/pages/chat/components/ChatHeader.tsx | 53 +-- src/pages/chat/components/ChatInput.css | 159 +++++++++ src/pages/chat/components/ChatInput.tsx | 336 ++++++++---------- src/pages/chat/components/ChatOutline.css | 175 +++++++++ src/pages/chat/components/ChatOutline.tsx | 68 ++-- src/pages/chat/components/ChatPageWeb.tsx | 101 +++--- .../chat/components/messages/MessageItem.css | 117 +++--- .../chat/components/messages/MessageItem.tsx | 138 +++---- .../chat/styles/chat-page-web-standard-pc.css | 11 +- src/pages/chat/styles/chat-page-web.css | 35 +- src/styles.css | 310 +--------------- src/styles/aura-theme.css | 103 +++--- vite.config.ts | 38 +- 39 files changed, 3280 insertions(+), 992 deletions(-) create mode 100644 src/api/membership.ts create mode 100644 src/components/Icon/index.tsx create mode 100644 src/components/Sidebar.css create mode 100644 src/pages/Pricing/PricingLogic.ts create mode 100644 src/pages/Pricing/components/PricingH5.tsx create mode 100644 src/pages/Pricing/components/PricingWeb.tsx create mode 100644 src/pages/Pricing/styles/pricing.css create mode 100644 src/pages/PricingPage.tsx create mode 100644 src/pages/Profile/ProfileLogic.ts create mode 100644 src/pages/Profile/components/ProfileH5.tsx create mode 100644 src/pages/Profile/components/ProfileWeb.tsx create mode 100644 src/pages/Profile/styles/profile.css create mode 100644 src/pages/ProfilePage.tsx create mode 100644 src/pages/chat/components/ChatBody.css create mode 100644 src/pages/chat/components/ChatHeader.css create mode 100644 src/pages/chat/components/ChatInput.css create mode 100644 src/pages/chat/components/ChatOutline.css diff --git a/src/App.tsx b/src/App.tsx index 32125bb..69259d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { Button, Drawer, Spin } from 'antd'; -import { MenuOutlined, SearchOutlined } from '@ant-design/icons'; +import { MenuOutlined, SearchOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import Sidebar from './components/Sidebar'; import CommandPalette from './components/CommandPalette'; import AgentList from './pages/AgentList'; @@ -13,6 +13,8 @@ import PointsMallPage from './pages/PointsMallPage'; import TeamsPage from './pages/TeamsPage'; import PromptLibraryPage from './pages/PromptLibraryPage'; import StatsPage from './pages/StatsPage'; +import ProfilePage from './pages/ProfilePage'; +import PricingPage from './pages/PricingPage'; import SharedSessionPage from './pages/SharedSessionPage'; import WorkflowsPage from './pages/WorkflowsPage'; import { useAuth } from './store/auth'; @@ -29,6 +31,7 @@ export default function App() { const location = useLocation(); const [paletteOpen, setPaletteOpen] = useState(false); const [mobileNavOpen, setMobileNavOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const isMobile = useIsMobile(); // 全局快捷键 Ctrl/⌘ + K @@ -58,6 +61,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> @@ -79,7 +84,11 @@ export default function App() {
{/* 只有编辑器全屏显示,其他页面均保留侧边栏 */} {!isMobile && (!location.pathname.startsWith('/agents/') || location.pathname.includes('/chat')) ? ( - setPaletteOpen(true)} /> + setPaletteOpen(true)} + collapsed={sidebarCollapsed} + onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)} + /> ) : null}
{isMobile && ( diff --git a/src/api/http.ts b/src/api/http.ts index 643a105..f122e46 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { clearUserStorage } from '../utils/storage'; -export const API_BASE_URL = 'https://www.tianchaoai.cc/api/v1/'; +export const API_BASE_URL = import.meta.env.DEV ? '/api/' : 'https://www.tianchaoai.cc/api/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.replace(/^\//, '')}`; diff --git a/src/api/index.ts b/src/api/index.ts index af2c0e2..be93997 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -15,4 +15,5 @@ export * from './stats'; export * from './llmProviders'; export * from './streamChat'; export * from './workflows'; +export * from './membership'; diff --git a/src/api/membership.ts b/src/api/membership.ts new file mode 100644 index 0000000..6645100 --- /dev/null +++ b/src/api/membership.ts @@ -0,0 +1,111 @@ +import { api } from './http'; + +/** + * 通用接口响应包装 + */ +export interface ApiResponse { + code: number; + message: string; + data: T; +} + +/** + * 分页列表包装 + */ +export interface ApiListData { + items: T[]; + total: number; +} + +/** + * 会员信息接口定义 (匹配后端最新数据结构) + */ +export interface MembershipInfo { + tier: 'trial' | 'pro' | 'ultra' | 'ent_basic' | 'ent_standard' | 'custom'; + tierName: string; + expireAt: number; + status: string; + isSub: boolean; + limits: { + tier: string; + name: string; + maxSubAccounts: number; + maxTokens: number; + maxAgents: number; + maxKBSize: number; + }; + usage: { + subAccountsCount: number; + tokensTotal: number; + tokensUsed: number; + }; +} + +/** + * 子账号信息接口定义 (匹配后端 snake_case 字段) + */ +export interface ChildMember { + id: string; // 关系 ID + parent_user_id: string; + child_user_id: string; + child_name: string; + child_phone: string; + status: string; + created_at: string; + updated_at: string; + allocatedTokens: number; + usedTokens: number; + authorizedAgentIds: string[] | null; +} + +/** + * 会员与账号管理 API + */ +export const MembershipAPI = { + /** + * 获取个人会员信息 + */ + getMe: () => api.get('/membership/me').then(r => r.data), + + /** + * 获取子账号列表 + */ + listMembers: () => api.get>>('/membership/members/list').then(r => r.data.data.items), + + /** + * 添加子账号 + * @param childId 用户ID + */ + addChild: (childId: string) => api.post>('/membership/members', { childId }).then(r => r.data), + + /** + * 移除子账号 + * @param relationId 关系ID + */ + removeChild: (relationId: string) => api.delete>(`/membership/members/${relationId}`).then(r => r.data), + + /** + * 分配算力配额 + */ + allocateQuota: (payload: { userId: string; resourceType: 'tokens'; amount: number }) => + api.post>('/membership/quota', payload).then(r => r.data), + + /** + * 授权资源(智能体/知识库)访问 + */ + authorizeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceId: string; level: 'read' | 'write' }) => + api.post>('/membership/access', payload).then(r => r.data), + + /** + * 取消资源授权 + */ + revokeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceId: string }) => + api.delete>('/membership/access', { data: payload }).then(r => r.data), + + /** + * 发起会员购买/升级 + * @returns 返回 payUrl + */ + subscribe: (payload: { tier: string; durationDays: number }) => + api.post>('/membership/subscribe', payload).then(r => r.data.data), +}; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx new file mode 100644 index 0000000..2b9299c --- /dev/null +++ b/src/components/Icon/index.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +export interface IconProps extends React.SVGProps { + color?: string; + size?: number | string; +} + +/** + * 附件按钮图标 + * @param color 图标颜色,默认为 #5C7480 + * @param size 图标大小,默认为 32 + */ +export function IconAttachment({ color = '#5C7480', size = 32, ...props }: IconProps) { + return ( + + + + ); +} + +/** + * AI按钮图标 + * @param color 图标颜色,默认为 #5C7480 + * @param size 图标大小,默认为 32 + */ +export function IconPrompt({ color = '#5C7480', size = 32, ...props }: IconProps) { + return ( + + + + + ); +} diff --git a/src/components/Sidebar.css b/src/components/Sidebar.css new file mode 100644 index 0000000..4fc036b --- /dev/null +++ b/src/components/Sidebar.css @@ -0,0 +1,252 @@ +.sidebar { + width: 240px; + background: var(--color-surface); + border-right: 1px solid var(--color-border); + color: var(--color-text); + display: flex; + flex-direction: column; + padding: 16px 12px; + height: 100vh; + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: visible; + position: relative; +} + +.sidebar.is-collapsed { + width: 68px; + padding: 16px 8px; +} + +.sidebar.is-collapsed .sidebar-brand { + justify-content: center; + padding: 8px 0 24px; +} + +.sidebar.is-collapsed .sidebar-search { + display: flex; + justify-content: center; + padding: 0; +} + +.sidebar.is-collapsed .sidebar-search-input { + width: 36px; + padding: 0 !important; + justify-content: center; +} + +.sidebar.is-collapsed .sidebar-nav-item { + justify-content: center; + padding: 10px 0; +} + +.sidebar.is-collapsed .sidebar-nav-icon { + margin: 0; +} + +.sidebar.is-collapsed .sidebar-user-card { + justify-content: center; + padding: 8px 0; + background: transparent; +} + +.sidebar-brand { + font-size: 16px; + font-weight: 700; + color: var(--color-text); + padding: 8px 10px 24px; + display: flex; + align-items: center; + gap: 10px; +} + +.sidebar-brand-logo { + width: 32px; + height: 32px; + flex: 0 0 auto; + object-fit: contain; +} + +.sidebar-brand-name { + font-size: 16px; + font-weight: 700; + flex: 1; +} + +.sidebar-brand-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + cursor: pointer; + color: var(--color-text-tertiary); + transition: all 0.2s; + background: var(--color-surface); + border: 1px solid var(--color-border); + position: absolute; + right: -12px; + top: 24px; + z-index: 100; + box-shadow: var(--shadow-sm); +} + +.sidebar-brand-toggle:hover { + background: var(--color-surface-2); + color: var(--color-text); +} + +.sidebar-search { + margin-bottom: 24px; + padding: 0 8px; +} + +.sidebar-search-input { + width: 100%; + height: 36px; + background: var(--color-bg) !important; + border: 1px solid var(--color-border) !important; + border-radius: 10px !important; + padding: 0 12px !important; + font-size: 13px !important; + display: flex; + align-items: center; + gap: 8px; + color: var(--color-text-tertiary); + cursor: pointer; + transition: all 0.2s; +} + +.sidebar-search-input:hover { + border-color: var(--color-border-strong) !important; +} + +.sidebar-search-icon { + font-size: 14px; +} + +.sidebar-search-placeholder { + flex: 1; +} + +.sidebar-search-suffix { + font-size: 11px; + color: var(--color-text-tertiary); + background: var(--color-surface); + padding: 1px 4px; + border-radius: 4px; + border: 1px solid var(--color-border); +} + +.sidebar-scroll { + flex: 1; + overflow-y: auto; + margin: 0 -4px; + padding: 0 4px; +} + +.sidebar-scroll::-webkit-scrollbar { + width: 4px; +} + +.sidebar-scroll::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 2px; +} + +.sidebar-scroll:hover::-webkit-scrollbar-thumb { + background: var(--color-border); +} + +.sidebar-nav-group { + margin-bottom: 20px; +} + +.sidebar-nav-label { + font-size: 12px; + font-weight: 600; + color: var(--color-text-tertiary); + padding: 0 12px 8px; +} + +.sidebar-nav-item { + padding: 10px 12px; + border-radius: 10px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 2px; + color: var(--color-text-secondary); + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; +} + +.sidebar-nav-item:hover { + background: var(--color-surface-2); + color: var(--color-text); +} + +.sidebar-nav-item.active { + background: var(--color-brand-soft); + color: var(--color-brand); + font-weight: 600; +} + +.sidebar-nav-icon { + font-size: 18px; + display: flex; +} + +.sidebar-user { + padding: 12px 8px 4px; +} + +.sidebar-user-card { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-radius: 12px; + cursor: pointer; + transition: background 0.2s; + background: #F7FCFA; +} + +.sidebar-user-card:hover { + background: var(--color-surface-2); +} + +.sidebar-user .sidebar-user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #5CCFC4 0%, #F2C94D 100%); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 14px; +} + +.sidebar-user-info { + flex: 1; + min-width: 0; +} + +.sidebar-user-name { + font-size: 13.5px; + font-weight: 600; + color: var(--color-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-user-role { + font-size: 11px; + color: var(--color-text-tertiary); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d2f9abf..3a78e0b 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -9,17 +9,21 @@ import { ApartmentOutlined, BarChartOutlined, TeamOutlined, - SunOutlined, - MoonOutlined, - LogoutOutlined + LogoutOutlined, + LeftOutlined, + RightOutlined, + UserOutlined, + CreditCardOutlined } from '@ant-design/icons'; import { useAuth } from '../store/auth'; -import { useTheme } from '../main'; import kaiwuIcon from '../assets/brand/kaiwu-icon-gradient-transparent.png'; +import './Sidebar.css'; interface Props { onOpenPalette?: () => void; onNavigate?: () => void; + collapsed?: boolean; + onToggleCollapse?: () => void; } const NAV_GROUPS: Array<{ @@ -51,111 +55,112 @@ const NAV_GROUPS: Array<{ { label: '商城', items: [ - { to: '/points-mall', icon: , label: 'Token商城' } + { to: '/points-mall', icon: , label: 'Token 商城' }, + { to: '/pricing', icon: , label: '会员计划' } ] } ]; -export default function Sidebar({ onOpenPalette, onNavigate }: Props) { +export default function Sidebar({ onOpenPalette, onNavigate, collapsed, onToggleCollapse }: Props) { const { user, logout } = useAuth(); const navigate = useNavigate(); - const { mode, toggle } = useTheme(); const isMac = typeof navigator !== 'undefined' && /mac|iphone|ipad|ipod/i.test(navigator.platform || ''); const cmdKey = isMac ? '⌘' : 'Ctrl'; return ( -