初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,483 @@
|
||||
import React, { useState } from 'react';
|
||||
import { mockMembershipPlans, mockApps, mockModels } from '@/mock/data';
|
||||
import type { MembershipPlan } from '@/types';
|
||||
|
||||
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
|
||||
const levelColors = [
|
||||
{ bg: 'var(--color-bg-tertiary)', accent: 'var(--color-text-secondary)', label: '免费版' },
|
||||
{ bg: 'var(--color-accent-light)', accent: 'var(--color-accent)', label: '专业版' },
|
||||
{ bg: '#FFF7E6', accent: '#E8922C', label: '企业版' },
|
||||
];
|
||||
|
||||
const typeLabels: Record<string, string> = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' };
|
||||
const typeBadgeColors: Record<string, string> = { skill: 'badge-success', agent: 'badge-info', mcp: 'badge-warning' };
|
||||
|
||||
// 所有收费应用 (price > 0, published)
|
||||
const allPaidApps = mockApps.filter(a => a.price > 0 && a.status === 'published');
|
||||
// 所有收费模型
|
||||
const allPaidModels = mockModels.filter(m => m.status === 'paid');
|
||||
|
||||
const Membership: React.FC = () => {
|
||||
const [plans, setPlans] = useState<MembershipPlan[]>(mockMembershipPlans);
|
||||
const [editingPlan, setEditingPlan] = useState<MembershipPlan | null>(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [autoDowngrade, setAutoDowngrade] = useState(true);
|
||||
const [autoRenew, setAutoRenew] = useState(true);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: '', level: 0, durationType: 'month' as 'month' | 'quarter' | 'year',
|
||||
price: 0, dailyFreeTokens: 0, features: [] as string[],
|
||||
freeAppIds: [] as string[], freeModelIds: [] as string[],
|
||||
});
|
||||
const [newFeature, setNewFeature] = useState('');
|
||||
|
||||
const handleEdit = (plan: MembershipPlan) => {
|
||||
setEditingPlan(plan);
|
||||
setForm({
|
||||
name: plan.name, level: plan.level, durationType: plan.durationType,
|
||||
price: plan.price, dailyFreeTokens: plan.dailyFreeTokens, features: [...plan.features],
|
||||
freeAppIds: [...(plan.freeAppIds || [])], freeModelIds: [...(plan.freeModelIds || [])],
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (editingPlan) {
|
||||
setPlans(prev => prev.map(p => p.id === editingPlan.id ? { ...p, ...form } : p));
|
||||
}
|
||||
setShowModal(false);
|
||||
setEditingPlan(null);
|
||||
};
|
||||
|
||||
const addFeature = () => {
|
||||
if (newFeature.trim()) {
|
||||
setForm({ ...form, features: [...form.features, newFeature.trim()] });
|
||||
setNewFeature('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeFeature = (idx: number) => {
|
||||
setForm({ ...form, features: form.features.filter((_, i) => i !== idx) });
|
||||
};
|
||||
|
||||
const toggleAppId = (appId: string) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
freeAppIds: prev.freeAppIds.includes(appId)
|
||||
? prev.freeAppIds.filter(id => id !== appId)
|
||||
: [...prev.freeAppIds, appId],
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleModelId = (modelId: string) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
freeModelIds: prev.freeModelIds.includes(modelId)
|
||||
? prev.freeModelIds.filter(id => id !== modelId)
|
||||
: [...prev.freeModelIds, modelId],
|
||||
}));
|
||||
};
|
||||
|
||||
// 根据 freeAppIds 获取应用名称列表(用于卡片展示)
|
||||
const getFreeAppNames = (plan: MembershipPlan): string[] => {
|
||||
return (plan.freeAppIds || []).map(id => allPaidApps.find(a => a.id === id)?.name).filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
const getFreeModelNames = (plan: MembershipPlan): string[] => {
|
||||
return (plan.freeModelIds || []).map(id => allPaidModels.find(m => m.id === id)?.name).filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
||||
<h1 className="admin-page-title">会员订阅体系管理</h1>
|
||||
|
||||
{/* Plan Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(340px, 1fr))', gap: 'var(--space-xl)', marginBottom: 'var(--space-2xl)' }}>
|
||||
{plans.map((plan, idx) => {
|
||||
const colors = levelColors[idx] || levelColors[0];
|
||||
const freeAppNames = getFreeAppNames(plan);
|
||||
const freeModelNames = getFreeModelNames(plan);
|
||||
return (
|
||||
<div key={plan.id} className="admin-card" style={{ marginBottom: 0, borderTop: `3px solid ${colors.accent}` }}>
|
||||
<div className="admin-card-header">
|
||||
<div>
|
||||
<span className="admin-badge" style={{ background: colors.bg, color: colors.accent, marginBottom: 'var(--space-sm)' }}>
|
||||
{colors.label}
|
||||
</span>
|
||||
<h3 style={{ fontSize: 'var(--text-lg)', fontWeight: 700, marginTop: 'var(--space-sm)' }}>{plan.name}</h3>
|
||||
</div>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleEdit(plan)}>
|
||||
<SVGIcon size={12}><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></SVGIcon>
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Pricing */}
|
||||
<div style={{ display: 'flex', gap: 'var(--space-lg)', marginBottom: 'var(--space-xl)' }}>
|
||||
<div style={{ flex: 1, textAlign: 'center', padding: 'var(--space-md)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-md)' }}>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>月付</div>
|
||||
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 700, color: colors.accent }}>{plan.price} 元</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, textAlign: 'center', padding: 'var(--space-md)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-md)' }}>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>季付</div>
|
||||
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 700, color: colors.accent }}>{Math.round(plan.price * 3 * 0.85)} 元</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, textAlign: 'center', padding: 'var(--space-md)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-md)' }}>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>年付</div>
|
||||
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 700, color: colors.accent }}>{Math.round(plan.price * 12 * 0.7)} 元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, marginBottom: 'var(--space-md)' }}>会员权益</div>
|
||||
<ul style={{ listStyle: 'none', padding: 0, display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>
|
||||
{plan.features.map((f, i) => (
|
||||
<li key={i} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)', fontSize: 'var(--text-sm)', color: 'var(--color-text-secondary)' }}>
|
||||
<SVGIcon size={14}><polyline points="20 6 9 17 4 12"/></SVGIcon>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Free tokens */}
|
||||
<div style={{ marginTop: 'var(--space-lg)', padding: 'var(--space-md)', background: 'var(--color-accent-subtle)', borderRadius: 'var(--radius-md)', fontSize: 'var(--text-sm)', color: 'var(--color-accent)' }}>
|
||||
每日免费额度: {plan.dailyFreeTokens.toLocaleString()} Token
|
||||
</div>
|
||||
|
||||
{/* Free apps & models summary */}
|
||||
{plan.level > 0 && (
|
||||
<div style={{ marginTop: 'var(--space-md)', display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>
|
||||
{freeAppNames.length > 0 && (
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', display: 'flex', alignItems: 'center', gap: 'var(--space-xs)' }}>
|
||||
<SVGIcon size={12}><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></SVGIcon>
|
||||
免费应用({freeAppNames.length}): {freeAppNames.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{freeModelNames.length > 0 && (
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', display: 'flex', alignItems: 'center', gap: 'var(--space-xs)' }}>
|
||||
<SVGIcon size={12}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon>
|
||||
免费模型({freeModelNames.length}): {freeModelNames.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Automation Rules */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">自动化规则配置</h3>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--space-xl)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: 'var(--space-xl)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-lg)' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, marginBottom: 'var(--space-xs)' }}>到期自动降级</div>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>会员到期后自动降级为免费版</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAutoDowngrade(!autoDowngrade)}
|
||||
style={{
|
||||
width: 48, height: 26, borderRadius: 13, border: 'none',
|
||||
background: autoDowngrade ? 'var(--color-accent)' : 'var(--color-text-disabled)',
|
||||
cursor: 'pointer', position: 'relative', transition: 'background var(--transition-fast)',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
position: 'absolute', top: 3, left: autoDowngrade ? 24 : 3,
|
||||
width: 20, height: 20, borderRadius: '50%', background: '#FFFFFF',
|
||||
transition: 'left var(--transition-fast)', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: 'var(--space-xl)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-lg)' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, marginBottom: 'var(--space-xs)' }}>自动续费默认开启</div>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>新会员订阅默认开启自动续费</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAutoRenew(!autoRenew)}
|
||||
style={{
|
||||
width: 48, height: 26, borderRadius: 13, border: 'none',
|
||||
background: autoRenew ? 'var(--color-accent)' : 'var(--color-text-disabled)',
|
||||
cursor: 'pointer', position: 'relative', transition: 'background var(--transition-fast)',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
position: 'absolute', top: 3, left: autoRenew ? 24 : 3,
|
||||
width: 20, height: 20, borderRadius: '50%', background: '#FFFFFF',
|
||||
transition: 'left var(--transition-fast)', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Free Application List */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">当前各等级会员免费应用总览</h3>
|
||||
</div>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>应用名称</th>
|
||||
<th>类型</th>
|
||||
<th>来源</th>
|
||||
<th>价格</th>
|
||||
<th>免费等级</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allPaidApps.map(app => {
|
||||
const freePlans = plans.filter(p => (p.freeAppIds || []).includes(app.id));
|
||||
return (
|
||||
<tr key={app.id}>
|
||||
<td style={{ fontWeight: 600 }}>{app.name}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${typeBadgeColors[app.type] || 'badge-neutral'}`}>
|
||||
{typeLabels[app.type] || app.type.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td><span className="admin-badge badge-neutral">{app.source === 'official' ? '官方' : '用户'}</span></td>
|
||||
<td style={{ fontFamily: 'var(--font-mono)' }}>{app.price} 元</td>
|
||||
<td>
|
||||
{freePlans.length > 0
|
||||
? freePlans.map(p => (
|
||||
<span key={p.id} className="admin-badge" style={{ background: 'var(--color-accent-light)', color: 'var(--color-accent)', marginRight: 4, marginBottom: 2 }}>
|
||||
{p.name}
|
||||
</span>
|
||||
))
|
||||
: <span style={{ color: 'var(--color-text-tertiary)', fontSize: 'var(--text-xs)' }}>无</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Free Model List */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">当前各等级会员免费大模型总览</h3>
|
||||
</div>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模型名称</th>
|
||||
<th>提供商</th>
|
||||
<th>每百万Token价格</th>
|
||||
<th>免费等级</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allPaidModels.map(model => {
|
||||
const freePlans = plans.filter(p => (p.freeModelIds || []).includes(model.id));
|
||||
return (
|
||||
<tr key={model.id}>
|
||||
<td style={{ fontWeight: 600 }}>{model.name}</td>
|
||||
<td>{model.providerName}</td>
|
||||
<td style={{ fontFamily: 'var(--font-mono)' }}>{model.pricePerMillionTokens} 元</td>
|
||||
<td>
|
||||
{freePlans.length > 0
|
||||
? freePlans.map(p => (
|
||||
<span key={p.id} className="admin-badge" style={{ background: 'var(--color-accent-light)', color: 'var(--color-accent)', marginRight: 4, marginBottom: 2 }}>
|
||||
{p.name}
|
||||
</span>
|
||||
))
|
||||
: <span style={{ color: 'var(--color-text-tertiary)', fontSize: 'var(--text-xs)' }}>无</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{showModal && (
|
||||
<div style={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||
background: 'rgba(0,0,0,0.4)', zIndex: 'var(--z-modal)',
|
||||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||||
paddingTop: '6vh',
|
||||
animation: 'fadeIn var(--transition-normal)',
|
||||
}} onClick={() => { setShowModal(false); setEditingPlan(null); }}>
|
||||
<div style={{
|
||||
background: '#FFFFFF', borderRadius: 'var(--radius-2xl)', width: 640, maxHeight: '84vh', overflow: 'auto',
|
||||
padding: 'var(--space-3xl)', boxShadow: 'var(--shadow-xl)',
|
||||
}} onClick={e => e.stopPropagation()}>
|
||||
<h3 style={{ fontSize: 'var(--text-lg)', fontWeight: 700, marginBottom: 'var(--space-2xl)' }}>
|
||||
编辑 {editingPlan?.name}
|
||||
</h3>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">月价格(元)</label>
|
||||
<input className="admin-form-input" type="number" value={form.price} onChange={e => setForm({ ...form, price: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">每日免费Token</label>
|
||||
<input className="admin-form-input" type="number" value={form.dailyFreeTokens} onChange={e => setForm({ ...form, dailyFreeTokens: Number(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">会员权益</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-sm)', marginBottom: 'var(--space-md)' }}>
|
||||
{form.features.map((f, i) => (
|
||||
<span key={i} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
padding: '2px 10px', borderRadius: 'var(--radius-full)',
|
||||
background: 'var(--color-accent-light)', color: 'var(--color-accent)',
|
||||
fontSize: 'var(--text-xs)', fontWeight: 500,
|
||||
}}>
|
||||
{f}
|
||||
<button onClick={() => removeFeature(i)} style={{ cursor: 'pointer', color: 'inherit', padding: 0, lineHeight: 1 }}>
|
||||
<SVGIcon size={12}><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></SVGIcon>
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
|
||||
<input
|
||||
className="admin-form-input"
|
||||
value={newFeature}
|
||||
onChange={e => setNewFeature(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addFeature(); } }}
|
||||
placeholder="输入权益名称,回车添加"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={addFeature}>添加</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Free Applications Selection */}
|
||||
<div style={{ marginTop: 'var(--space-2xl)' }}>
|
||||
<label className="admin-form-label" style={{ marginBottom: 'var(--space-md)', display: 'block' }}>
|
||||
收费应用免费权益
|
||||
<span style={{ fontWeight: 400, color: 'var(--color-text-tertiary)', marginLeft: 'var(--space-sm)', fontSize: 'var(--text-xs)' }}>
|
||||
勾选后会员可免费使用
|
||||
</span>
|
||||
</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: 200, overflowY: 'auto', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', padding: 'var(--space-sm)' }}>
|
||||
{allPaidApps.length === 0 && (
|
||||
<div style={{ padding: 'var(--space-md)', color: 'var(--color-text-tertiary)', fontSize: 'var(--text-sm)', textAlign: 'center' }}>暂无收费应用</div>
|
||||
)}
|
||||
{allPaidApps.map(app => {
|
||||
const checked = form.freeAppIds.includes(app.id);
|
||||
return (
|
||||
<label
|
||||
key={app.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 'var(--space-sm)',
|
||||
padding: 'var(--space-sm) var(--space-md)',
|
||||
borderRadius: 'var(--radius-sm)', cursor: 'pointer',
|
||||
background: checked ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
transition: 'background var(--transition-fast)',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleAppId(app.id)}
|
||||
style={{
|
||||
width: 16, height: 16, accentColor: 'var(--color-accent)',
|
||||
cursor: 'pointer', flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 'var(--text-sm)', fontWeight: 500, flex: 1 }}>{app.name}</span>
|
||||
<span className={`admin-badge ${typeBadgeColors[app.type] || 'badge-neutral'}`} style={{ fontSize: 'var(--text-2xs)' }}>
|
||||
{typeLabels[app.type] || app.type.toUpperCase()}
|
||||
</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', minWidth: 60, textAlign: 'right' }}>
|
||||
{app.price} 元
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', marginTop: 'var(--space-xs)' }}>
|
||||
已选 {form.freeAppIds.length}/{allPaidApps.length} 个应用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Free Models Selection */}
|
||||
<div style={{ marginTop: 'var(--space-2xl)' }}>
|
||||
<label className="admin-form-label" style={{ marginBottom: 'var(--space-md)', display: 'block' }}>
|
||||
收费大模型免费权益
|
||||
<span style={{ fontWeight: 400, color: 'var(--color-text-tertiary)', marginLeft: 'var(--space-sm)', fontSize: 'var(--text-xs)' }}>
|
||||
勾选后会员可免费调用,不消耗Token
|
||||
</span>
|
||||
</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: 200, overflowY: 'auto', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', padding: 'var(--space-sm)' }}>
|
||||
{allPaidModels.length === 0 && (
|
||||
<div style={{ padding: 'var(--space-md)', color: 'var(--color-text-tertiary)', fontSize: 'var(--text-sm)', textAlign: 'center' }}>暂无收费模型</div>
|
||||
)}
|
||||
{allPaidModels.map(model => {
|
||||
const checked = form.freeModelIds.includes(model.id);
|
||||
return (
|
||||
<label
|
||||
key={model.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 'var(--space-sm)',
|
||||
padding: 'var(--space-sm) var(--space-md)',
|
||||
borderRadius: 'var(--radius-sm)', cursor: 'pointer',
|
||||
background: checked ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
transition: 'background var(--transition-fast)',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleModelId(model.id)}
|
||||
style={{
|
||||
width: 16, height: 16, accentColor: 'var(--color-accent)',
|
||||
cursor: 'pointer', flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 'var(--text-sm)', fontWeight: 500, flex: 1 }}>{model.name}</span>
|
||||
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{model.providerName}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', minWidth: 80, textAlign: 'right' }}>
|
||||
{model.pricePerMillionTokens} 元/百万Token
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', marginTop: 'var(--space-xs)' }}>
|
||||
已选 {form.freeModelIds.length}/{allPaidModels.length} 个模型
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<button className="admin-btn admin-btn-primary" onClick={handleSave}>保存</button>
|
||||
<button className="admin-btn admin-btn-outline" onClick={() => { setShowModal(false); setEditingPlan(null); }}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Membership;
|
||||
Reference in new issue
Block a user