初始提交: Qihu-AIFlow v2.0.1 完整项目

- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
This commit is contained in:
qyyfw committed 2026-07-02 14:40:46 +08:00
commit 78ff261b5a
199 files changed
+69126

No files matched your search

+920
View File
@@ -0,0 +1,920 @@
import React, { useState, useMemo } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { profileAPI } from '@/api';
import { mockOrders, mockMembershipPlans, mockUserCoupons, mockApps, mockOperationLogs } from '@/mock/data';
import type { Order, UserCoupon, MarketApp, OperationLog } from '@/types';
import './ProfilePage.css';
// ===== Mock API Keys =====
interface ApiKey {
id: string;
prefix: string;
fullKey: string;
createdAt: string;
status: 'active' | 'disabled';
}
const mockApiKeys: ApiKey[] = [
{ id: 'ak1', prefix: 'sk-aiflow-****a1b2', fullKey: 'sk-aiflow-u1-x8j3a1b2c3d4e5f6g7h8i9j0k1l2', createdAt: '2026-03-15T08:00:00Z', status: 'active' },
{ id: 'ak2', prefix: 'sk-aiflow-****c3d4', fullKey: 'sk-aiflow-u1-y9k4c3d4e5f6g7h8i9j0k1l2m3n4', createdAt: '2026-04-20T10:00:00Z', status: 'active' },
{ id: 'ak3', prefix: 'sk-aiflow-****e5f6', fullKey: 'sk-aiflow-u1-z0l5e5f6g7h8i9j0k1l2m3n4o5p6', createdAt: '2026-05-10T14:00:00Z', status: 'disabled' },
];
// ===== SVG Icons =====
const SvgIcons = {
// Password
lock: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
),
phone: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>
),
mail: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>
</svg>
),
log: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>
</svg>
),
order: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="12" y1="17" x2="8" y2="17"/>
</svg>
),
member: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
),
coupon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 12V20H4V12"/><path d="M22 7H2v5h20V7z"/><circle cx="12" cy="7" r="2"/><circle cx="12" cy="17" r="2"/><line x1="12" y1="9" x2="12" y2="15"/>
</svg>
),
apps: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/>
</svg>
),
key: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
</svg>
),
copy: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
),
check: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
eye: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
</svg>
),
calendar: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
),
diamond: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="4" transform="rotate(45 12 12)"/>
</svg>
),
close: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
),
checkCircle: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
</svg>
),
};
// ===== Tabs =====
type ProfileTab = 'security' | 'orders' | 'membership' | 'coupons' | 'apps' | 'apikeys';
interface TabDef {
key: ProfileTab;
label: string;
icon: JSX.Element;
}
const tabs: TabDef[] = [
{ key: 'security', label: '账号安全', icon: SvgIcons.lock },
{ key: 'orders', label: '我的订单', icon: SvgIcons.order },
{ key: 'membership', label: '我的会员', icon: SvgIcons.member },
{ key: 'coupons', label: '我的优惠券', icon: SvgIcons.coupon },
{ key: 'apps', label: '我的已购应用', icon: SvgIcons.apps },
{ key: 'apikeys', label: 'API密钥管理', icon: SvgIcons.key },
];
// ===== Helper =====
function formatDate(dateStr: string): string {
const d = new Date(dateStr);
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
function formatShortDate(dateStr: string): string {
const d = new Date(dateStr);
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
// ===== ProfilePage =====
const ProfilePage: React.FC = () => {
const { user } = useAuth();
const [activeTab, setActiveTab] = useState<ProfileTab>('security');
// Security form states
const [passwordForm, setPasswordForm] = useState({ old: '', newPass: '', confirm: '' });
const [bindForm, setBindForm] = useState({ type: 'phone' as 'phone' | 'email', value: '' });
// Order filter
const [orderFilter, setOrderFilter] = useState<Order['status'] | 'all'>('all');
// Coupon filter
const [couponFilter, setCouponFilter] = useState<UserCoupon['status'] | 'all'>('all');
// API Key
const [apiKeys, setApiKeys] = useState<ApiKey[]>(mockApiKeys);
const [showKeyId, setShowKeyId] = useState<string | null>(null);
const [copiedId, setCopiedId] = useState<string | null>(null);
if (!user) return null;
const memberPlan = mockMembershipPlans.find(p => p.id === user.membership?.planId);
// User's purchased apps (via mockOrders -> app type orders)
const purchasedAppOrders = useMemo(() => mockOrders.filter(o => o.userId === 'u1' && o.orderType === 'app' && o.status === 'paid'), []);
const purchasedApps: MarketApp[] = useMemo(() => {
return purchasedAppOrders
.map(o => mockApps.find(a => a.name === o.itemName))
.filter((a): a is MarketApp => !!a);
}, [purchasedAppOrders]);
// Login logs (recent 5)
const loginLogs = useMemo(() =>
mockOperationLogs.filter(l => l.userId === 'u1').slice(0, 5),
[]);
// Filter orders
const filteredOrders = useMemo(() => {
const userOrders = mockOrders.filter(o => o.userId === 'u1');
return orderFilter === 'all' ? userOrders : userOrders.filter(o => o.status === orderFilter);
}, [orderFilter]);
// Filter coupons
const filteredCoupons = useMemo(() => {
return couponFilter === 'all' ? mockUserCoupons : mockUserCoupons.filter(c => c.status === couponFilter);
}, [couponFilter]);
const handleCopyKey = (key: ApiKey) => {
navigator.clipboard.writeText(key.fullKey);
setCopiedId(key.id);
setTimeout(() => setCopiedId(null), 2000);
};
const handleGenerateKey = () => {
const newKey: ApiKey = {
id: `ak${Date.now()}`,
prefix: `sk-aiflow-****${Math.random().toString(36).slice(2, 6)}`,
fullKey: `sk-aiflow-u1-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`,
createdAt: new Date().toISOString(),
status: 'active',
};
setApiKeys(prev => [newKey, ...prev]);
};
const handleCopyPrefix = (key: ApiKey) => {
navigator.clipboard.writeText(key.prefix);
setCopiedId(key.id);
setTimeout(() => setCopiedId(null), 2000);
};
const handleToggleKeyStatus = (keyId: string) => {
setApiKeys(prev => prev.map(k =>
k.id === keyId ? { ...k, status: k.status === 'active' ? 'disabled' as const : 'active' as const } : k
));
};
const orderStatusLabels: Record<Order['status'] | 'all', string> = {
all: '全部',
pending: '待支付',
paid: '已支付',
refunded: '已退款',
};
const orderStatusColors: Record<Order['status'], string> = {
pending: 'var(--color-warning)',
paid: 'var(--color-success)',
refunded: 'var(--color-text-tertiary)',
};
const orderTypeLabels: Record<Order['orderType'], string> = {
membership: '会员',
app: '应用',
service: '服务',
};
const couponStatusConfig: Record<UserCoupon['status'], { label: string; color: string; bg: string }> = {
unused: { label: '未使用', color: 'var(--color-success)', bg: 'var(--color-success-light)' },
used: { label: '已使用', color: 'var(--color-text-tertiary)', bg: 'var(--color-bg-tertiary)' },
expired: { label: '已过期', color: 'var(--color-error)', bg: 'var(--color-error-light)' },
};
const getCouponDescription = (coupon: UserCoupon['coupon']) => {
if (coupon.type === 'fixed') return `满${coupon.minAmount}减${coupon.value}`;
return `${coupon.value}折券,满${coupon.minAmount}可用`;
};
const getLevelGradient = (level: number) => {
const gradients: Record<number, string> = {
0: 'linear-gradient(135deg, #8B919E, #BFC4CE)',
2: 'linear-gradient(135deg, #5B5FED, #8B8DFF)',
3: 'linear-gradient(135deg, #F59E0B, #FBBF24)',
};
return gradients[level] || gradients[0];
};
return (
<div className="profile-page">
{/* Header */}
<div className="profile-header">
<div className="profile-user-card">
<div
className="profile-avatar"
style={{ background: getLevelGradient(user.membership?.level ?? 0) }}
>
<span>{user.username.charAt(0).toUpperCase()}</span>
</div>
<div className="profile-user-info">
<div className="profile-username-row">
<h1 className="profile-username">{user.username}</h1>
{user.membership && (
<span
className="profile-level-badge"
style={{ background: getLevelGradient(user.membership.level) }}
>
{user.membership.planName}
</span>
)}
</div>
{user.membership && (
<p className="profile-expire">
<span className="profile-expire-icon">{SvgIcons.calendar}</span>
会员到期:{formatShortDate(user.membership.expireAt)}
</p>
)}
</div>
</div>
</div>
{/* Body */}
<div className="profile-body">
{/* Left Tabs */}
<nav className="profile-tabs">
{tabs.map(tab => (
<button
key={tab.key}
className={`profile-tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => setActiveTab(tab.key)}
>
<span className="profile-tab-icon">{tab.icon}</span>
<span className="profile-tab-label">{tab.label}</span>
</button>
))}
</nav>
{/* Right Content */}
<div className="profile-content">
{activeTab === 'security' && (
<SecurityContent
passwordForm={passwordForm}
setPasswordForm={setPasswordForm}
bindForm={bindForm}
setBindForm={setBindForm}
loginLogs={loginLogs}
/>
)}
{activeTab === 'orders' && (
<OrdersContent
orders={filteredOrders}
filter={orderFilter}
setFilter={setOrderFilter}
statusLabels={orderStatusLabels}
statusColors={orderStatusColors}
typeLabels={orderTypeLabels}
/>
)}
{activeTab === 'membership' && (
<MembershipContent user={user} memberPlan={memberPlan} />
)}
{activeTab === 'coupons' && (
<CouponsContent
coupons={filteredCoupons}
filter={couponFilter}
setFilter={setCouponFilter}
getDescription={getCouponDescription}
statusConfig={couponStatusConfig}
/>
)}
{activeTab === 'apps' && (
<AppsContent apps={purchasedApps} purchasedOrders={purchasedAppOrders} />
)}
{activeTab === 'apikeys' && (
<ApiKeysContent
apiKeys={apiKeys}
showKeyId={showKeyId}
setShowKeyId={setShowKeyId}
copiedId={copiedId}
onCopy={handleCopyKey}
onCopyPrefix={handleCopyPrefix}
onGenerate={handleGenerateKey}
onToggleStatus={handleToggleKeyStatus}
/>
)}
</div>
</div>
</div>
);
};
// ===== Security Content =====
interface SecurityContentProps {
passwordForm: { old: string; newPass: string; confirm: string };
setPasswordForm: React.Dispatch<React.SetStateAction<{ old: string; newPass: string; confirm: string }>>;
bindForm: { type: 'phone' | 'email'; value: string };
setBindForm: React.Dispatch<React.SetStateAction<{ type: 'phone' | 'email'; value: string }>>;
loginLogs: OperationLog[];
}
const SecurityContent: React.FC<SecurityContentProps> = ({ passwordForm, setPasswordForm, bindForm, setBindForm, loginLogs }) => {
const [pwdMsg, setPwdMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [pwdLoading, setPwdLoading] = useState(false);
const handleChangePassword = async () => {
setPwdMsg(null);
if (!passwordForm.old.trim()) {
setPwdMsg({ type: 'error', text: '请输入旧密码' });
return;
}
if (!passwordForm.newPass.trim() || passwordForm.newPass.length < 6) {
setPwdMsg({ type: 'error', text: '新密码长度至少为6位' });
return;
}
if (passwordForm.newPass !== passwordForm.confirm) {
setPwdMsg({ type: 'error', text: '两次输入的新密码不一致' });
return;
}
setPwdLoading(true);
try {
await profileAPI.updatePassword({
old_password: passwordForm.old,
new_password: passwordForm.newPass,
});
setPwdMsg({ type: 'success', text: '密码修改成功' });
setPasswordForm({ old: '', newPass: '', confirm: '' });
} catch (e: any) {
setPwdMsg({ type: 'error', text: e.message || e.detail || '密码修改失败' });
} finally {
setPwdLoading(false);
}
};
return (
<div className="security-content">
{/* Change Password */}
<section className="profile-section">
<h3 className="section-title">
<span className="section-icon">{SvgIcons.lock}</span>
修改密码
</h3>
{pwdMsg && (
<div className={`form-message ${pwdMsg.type}`}>
{pwdMsg.text}
</div>
)}
<div className="form-grid">
<div className="form-item">
<label className="form-label-small">旧密码</label>
<input
type="password"
className="form-input"
placeholder="请输入旧密码"
value={passwordForm.old}
onChange={e => setPasswordForm(p => ({ ...p, old: e.target.value }))}
/>
</div>
<div className="form-item">
<label className="form-label-small">新密码</label>
<input
type="password"
className="form-input"
placeholder="请输入新密码(至少6位)"
value={passwordForm.newPass}
onChange={e => setPasswordForm(p => ({ ...p, newPass: e.target.value }))}
/>
</div>
<div className="form-item">
<label className="form-label-small">确认新密码</label>
<input
type="password"
className="form-input"
placeholder="请再次输入新密码"
value={passwordForm.confirm}
onChange={e => setPasswordForm(p => ({ ...p, confirm: e.target.value }))}
/>
</div>
<div className="form-item form-item-btn">
<button className="btn-primary" onClick={handleChangePassword} disabled={pwdLoading}>
{pwdLoading ? '修改中...' : '修改密码'}
</button>
</div>
</div>
</section>
{/* Bind Phone/Email */}
<section className="profile-section">
<h3 className="section-title">
<span className="section-icon">{SvgIcons.phone}</span>
绑定手机 / 邮箱
</h3>
<div className="bind-container">
<div className="bind-type-toggle">
<button
className={`bind-type-btn ${bindForm.type === 'phone' ? 'active' : ''}`}
onClick={() => setBindForm(f => ({ ...f, type: 'phone' }))}
>
{SvgIcons.phone}
<span>手机</span>
</button>
<button
className={`bind-type-btn ${bindForm.type === 'email' ? 'active' : ''}`}
onClick={() => setBindForm(f => ({ ...f, type: 'email' }))}
>
{SvgIcons.mail}
<span>邮箱</span>
</button>
</div>
<div className="bind-input-row">
<div className="form-item" style={{ flex: 1 }}>
<label className="form-label-small">
{bindForm.type === 'phone' ? '手机号' : '邮箱地址'}
</label>
<input
type={bindForm.type === 'phone' ? 'tel' : 'email'}
className="form-input"
placeholder={bindForm.type === 'phone' ? '请输入手机号' : '请输入邮箱地址'}
value={bindForm.value}
onChange={e => setBindForm(f => ({ ...f, value: e.target.value }))}
/>
</div>
<div className="form-item form-item-btn">
<label className="form-label-small">&nbsp;</label>
<button className="btn-secondary">获取验证码</button>
</div>
</div>
<div className="form-item form-item-btn">
<button className="btn-primary">确认绑定</button>
</div>
</div>
</section>
{/* Login Logs */}
<section className="profile-section">
<h3 className="section-title">
<span className="section-icon">{SvgIcons.log}</span>
最近登录日志
</h3>
<div className="simple-table-wrapper">
<table className="simple-table">
<thead>
<tr>
<th>操作</th>
<th>目标</th>
<th>IP地址</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{loginLogs.map(log => (
<tr key={log.id}>
<td>{log.action}</td>
<td className="td-muted">{log.target}</td>
<td className="td-mono">{log.ip}</td>
<td className="td-muted">{formatDate(log.createdAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
);
};
// ===== Orders Content =====
interface OrdersContentProps {
orders: Order[];
filter: Order['status'] | 'all';
setFilter: React.Dispatch<React.SetStateAction<Order['status'] | 'all'>>;
statusLabels: Record<Order['status'] | 'all', string>;
statusColors: Record<Order['status'], string>;
typeLabels: Record<Order['orderType'], string>;
}
const OrdersContent: React.FC<OrdersContentProps> = ({ orders, filter, setFilter, statusLabels, statusColors, typeLabels }) => {
return (
<div className="tab-panel-content">
{/* Filter Tabs */}
<div className="filter-tabs">
{(Object.keys(statusLabels) as (Order['status'] | 'all')[]).map(key => (
<button
key={key}
className={`filter-tab ${filter === key ? 'active' : ''}`}
onClick={() => setFilter(key)}
>
{statusLabels[key]}
</button>
))}
</div>
{/* Table */}
<div className="simple-table-wrapper">
<table className="simple-table">
<thead>
<tr>
<th>订单号</th>
<th>类型</th>
<th>商品名</th>
<th>金额</th>
<th>支付方式</th>
<th>状态</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{orders.map(order => (
<tr key={order.id}>
<td className="td-mono">{order.id}</td>
<td>
<span className="order-type-tag">{typeLabels[order.orderType]}</span>
</td>
<td>{order.itemName}</td>
<td className="td-price">¥{order.amount.toFixed(2)}</td>
<td className="td-muted">{order.paymentMethod === 'wechat' ? '微信支付' : '支付宝'}</td>
<td>
<span className="status-dot" style={{ backgroundColor: statusColors[order.status] }} />
{statusLabels[order.status]}
</td>
<td className="td-muted">{formatDate(order.createdAt)}</td>
</tr>
))}
{orders.length === 0 && (
<tr><td colSpan={7} className="td-empty">暂无订单</td></tr>
)}
</tbody>
</table>
</div>
</div>
);
};
// ===== Membership Content =====
interface MembershipContentProps {
user: NonNullable<ReturnType<typeof useAuth>['user']>;
memberPlan: typeof mockMembershipPlans[0] | undefined;
}
const MembershipContent: React.FC<MembershipContentProps> = ({ user, memberPlan }) => {
const [autoRenew, setAutoRenew] = useState(user.membership?.autoRenew ?? false);
// 使用 memberPlan 或从 user.membership 获取 level,判断是否付费会员
const effectiveLevel = memberPlan?.level ?? user.membership?.level ?? 0;
const effectiveName = memberPlan?.name || user.membership?.planName || '免费版';
const hasPaidMembership = effectiveLevel > 0;
return (
<div className="tab-panel-content">
<div className="membership-card">
<div className="membership-card-header">
<div className="membership-card-icon">
{SvgIcons.diamond}
</div>
<div className="membership-card-title">
<h2>{effectiveName}</h2>
{hasPaidMembership && (
<span className="membership-level-tag">Lv.{effectiveLevel}</span>
)}
</div>
</div>
{hasPaidMembership && user.membership && (
<>
{memberPlan && (
<div className="membership-stats">
<div className="membership-stat">
<span className="stat-label">每日可用Token</span>
<span className="stat-value">{memberPlan.dailyFreeTokens.toLocaleString()}</span>
</div>
<div className="membership-stat">
<span className="stat-label">会员到期</span>
<span className="stat-value">{formatShortDate(user.membership.expireAt)}</span>
</div>
<div className="membership-stat">
<span className="stat-label">月费</span>
<span className="stat-value">¥{memberPlan.price}/月</span>
</div>
</div>
)}
{!memberPlan && user.membership && (
<div className="membership-stats">
<div className="membership-stat">
<span className="stat-label">会员到期</span>
<span className="stat-value">{formatShortDate(user.membership.expireAt)}</span>
</div>
</div>
)}
{memberPlan && (
<div className="membership-features">
<h4 className="features-title">会员权益</h4>
<ul className="features-list">
{memberPlan.features.map((f, i) => (
<li key={i} className="feature-item">
<span className="feature-check">{SvgIcons.checkCircle}</span>
{f}
</li>
))}
</ul>
</div>
)}
<div className="membership-auto-renew">
<span className="auto-renew-label">自动续费</span>
<label className="toggle-switch">
<input
type="checkbox"
checked={autoRenew}
onChange={e => setAutoRenew(e.target.checked)}
/>
<span className="toggle-slider" />
</label>
</div>
<div className="membership-actions">
<button className="btn-primary" onClick={() => { window.location.hash = '#/pricing'; }}>续费会员</button>
<button className="btn-secondary" onClick={() => { window.location.hash = '#/pricing'; }}>升级会员</button>
</div>
</>
)}
{!hasPaidMembership && (
<div className="membership-empty">
<p>您当前使用的是免费版,升级会员享受更多权益</p>
<button
className="btn-primary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
window.location.hash = '#/pricing';
}}
>
开通会员
</button>
</div>
)}
</div>
</div>
);
};
// ===== Coupons Content =====
interface CouponsContentProps {
coupons: UserCoupon[];
filter: UserCoupon['status'] | 'all';
setFilter: React.Dispatch<React.SetStateAction<UserCoupon['status'] | 'all'>>;
getDescription: (coupon: UserCoupon['coupon']) => string;
statusConfig: Record<UserCoupon['status'], { label: string; color: string; bg: string }>;
}
const CouponsContent: React.FC<CouponsContentProps> = ({ coupons, filter, setFilter, getDescription, statusConfig }) => {
const filterLabels: Record<UserCoupon['status'] | 'all', string> = {
all: '全部',
unused: '未使用',
used: '已使用',
expired: '已过期',
};
return (
<div className="tab-panel-content">
<div className="filter-tabs">
{(Object.keys(filterLabels) as (UserCoupon['status'] | 'all')[]).map(key => (
<button
key={key}
className={`filter-tab ${filter === key ? 'active' : ''}`}
onClick={() => setFilter(key)}
>
{filterLabels[key]}
</button>
))}
</div>
<div className="coupon-list">
{coupons.map(uc => {
const cfg = statusConfig[uc.status];
return (
<div key={uc.id} className={`coupon-item ${uc.status !== 'unused' ? 'coupon-disabled' : ''}`}>
<div className="coupon-left">
<div className="coupon-code">{uc.coupon.code}</div>
<div className="coupon-desc">{getDescription(uc.coupon)}</div>
<div className="coupon-date">
有效期:{formatShortDate(uc.coupon.validFrom)} ~ {formatShortDate(uc.coupon.validTo)}
</div>
</div>
<div className="coupon-right">
<span
className="coupon-status"
style={{ color: cfg.color, backgroundColor: cfg.bg }}
>
{cfg.label}
</span>
{uc.status === 'unused' && (
<button className="btn-coupon-use">去使用</button>
)}
</div>
</div>
);
})}
{coupons.length === 0 && (
<div className="content-empty">暂无优惠券</div>
)}
</div>
</div>
);
};
// ===== Apps Content =====
interface AppsContentProps {
apps: MarketApp[];
purchasedOrders: Order[];
}
const AppsContent: React.FC<AppsContentProps> = ({ apps, purchasedOrders }) => {
const appTypeLabels: Record<string, string> = {
skill: '技能',
agent: '智能体',
mcp: 'MCP',
};
const findOrderDate = (appName: string) => {
const order = purchasedOrders.find(o => o.itemName === appName);
return order ? formatDate(order.createdAt) : '';
};
return (
<div className="tab-panel-content">
<div className="apps-grid">
{apps.map(app => (
<div key={app.id} className="app-card">
<div className="app-card-header">
<div className="app-card-icon">
<span>{app.icon || app.name.charAt(0)}</span>
</div>
<div className="app-card-info">
<h4 className="app-card-name">{app.name}</h4>
<div className="app-card-meta">
<span className="app-type-tag">{appTypeLabels[app.type] || app.type}</span>
<span className="app-source-tag">{app.source === 'official' ? '官方' : '用户'}</span>
</div>
</div>
</div>
<p className="app-card-desc">{app.description}</p>
<div className="app-card-footer">
<span className="app-purchase-date">
{SvgIcons.calendar}
{findOrderDate(app.name)}
</span>
</div>
</div>
))}
{apps.length === 0 && (
<div className="content-empty">暂未购买任何应用</div>
)}
</div>
</div>
);
};
// ===== API Keys Content =====
interface ApiKeysContentProps {
apiKeys: ApiKey[];
showKeyId: string | null;
setShowKeyId: React.Dispatch<React.SetStateAction<string | null>>;
copiedId: string | null;
onCopy: (key: ApiKey) => void;
onCopyPrefix: (key: ApiKey) => void;
onGenerate: () => void;
onToggleStatus: (keyId: string) => void;
}
const ApiKeysContent: React.FC<ApiKeysContentProps> = ({
apiKeys, showKeyId, setShowKeyId, copiedId, onCopy, onCopyPrefix, onGenerate, onToggleStatus,
}) => {
return (
<div className="tab-panel-content">
<div className="apikey-header">
<div className="apikey-info">
<h3>API密钥</h3>
<p className="apikey-desc">使用API密钥访问AIFlow接口,请妥善保管您的密钥</p>
</div>
<button className="btn-primary" onClick={onGenerate}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
生成新API Key
</button>
</div>
<div className="apikey-list">
<div className="apikey-list-header">
<span>密钥前缀</span>
<span>创建时间</span>
<span>状态</span>
<span>操作</span>
</div>
{apiKeys.map(key => (
<div key={key.id} className="apikey-row">
<div className="apikey-prefix-col">
<code className="apikey-prefix">{key.prefix}</code>
{showKeyId === key.id && (
<div className="apikey-full-reveal">
<code>{key.fullKey}</code>
</div>
)}
</div>
<span className="apikey-date">{formatDate(key.createdAt)}</span>
<span>
<span className={`apikey-status ${key.status}`}>
{key.status === 'active' ? '活跃' : '已禁用'}
</span>
</span>
<div className="apikey-actions">
<button
className="apikey-action-btn"
onClick={() => {
if (showKeyId === key.id) {
setShowKeyId(null);
} else {
setShowKeyId(key.id);
onCopyPrefix(key);
}
}}
title={showKeyId === key.id ? '隐藏密钥' : '显示密钥'}
>
{SvgIcons.eye}
</button>
<button
className="apikey-action-btn"
onClick={() => {
if (showKeyId === key.id || showKeyId === null) {
onCopy(key);
} else {
onCopyPrefix(key);
}
}}
title="复制密钥"
>
{copiedId === key.id ? SvgIcons.check : SvgIcons.copy}
</button>
<button
className={`apikey-action-btn ${key.status === 'active' ? 'danger' : ''}`}
onClick={() => onToggleStatus(key.id)}
title={key.status === 'active' ? '禁用' : '启用'}
>
{key.status === 'active' ? '禁用' : '启用'}
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default ProfilePage;