Phase G4: Distribution Builder - SaaS product line mgmt, license keys, config export, frontend UI
This commit is contained in:
1 parent
d2e3147fb8
commit
0486210133
8 files changed
+1671
-258
No files matched your search
@@ -0,0 +1,493 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { adminAPI } from '@/api';
|
||||
|
||||
interface Distribution {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
brand_name: string;
|
||||
brand_logo_url: string;
|
||||
brand_color: string;
|
||||
system_prompt: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
mcp_extensions: any[];
|
||||
workflow_ids: string[];
|
||||
knowledge_base_ids: string[];
|
||||
agent_ids: string[];
|
||||
features: string[];
|
||||
status: string;
|
||||
version: string;
|
||||
download_count: number;
|
||||
size_bytes: number;
|
||||
license_count?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface License {
|
||||
id: string;
|
||||
distribution_id: string;
|
||||
distribution_name: string;
|
||||
org_name: string;
|
||||
licensee_email: string;
|
||||
license_key: string;
|
||||
features: string[];
|
||||
max_users: number;
|
||||
max_executions: number;
|
||||
usage_count: number;
|
||||
expiry_date: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
total: number;
|
||||
draft: number;
|
||||
active: number;
|
||||
archived: number;
|
||||
total_downloads: number;
|
||||
total_licenses: number;
|
||||
active_licenses: number;
|
||||
}
|
||||
|
||||
interface Templates {
|
||||
workflows: any[];
|
||||
agents: any[];
|
||||
knowledge_bases: any[];
|
||||
mcp_tools: any[];
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = { draft: '草稿', active: '已发布', archived: '已归档' };
|
||||
const STATUS_COLORS: Record<string, string> = { draft: '#8b949e', active: '#3fb950', archived: '#f85149' };
|
||||
const FEATURE_LABELS: Record<string, string> = {
|
||||
chat: 'AI 对话', code_exec: '代码执行', knowledge_search: '知识库检索',
|
||||
agent_collab: 'Agent 协作', automation: '自动化', webhook: 'Webhook',
|
||||
};
|
||||
|
||||
export default function DistributionBuilder() {
|
||||
const [distributions, setDistributions] = useState<Distribution[]>([]);
|
||||
const [stats, setStats] = useState<Stats>({ total: 0, draft: 0, active: 0, archived: 0, total_downloads: 0, total_licenses: 0, active_licenses: 0 });
|
||||
const [templates, setTemplates] = useState<Templates>({ workflows: [], agents: [], knowledge_bases: [], mcp_tools: [] });
|
||||
const [licenses, setLicenses] = useState<License[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [tab, setTab] = useState<'distributions' | 'licenses'>('distributions');
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [showLicense, setShowLicense] = useState(false);
|
||||
const [selectedDistro, setSelectedDistro] = useState<string>('');
|
||||
const [editDistro, setEditDistro] = useState<Distribution | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [licPage, setLicPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [licTotalPages, setLicTotalPages] = useState(1);
|
||||
|
||||
// Form state
|
||||
const [form, setForm] = useState({
|
||||
name: '', description: '', brand_name: '', brand_color: '#6366f1',
|
||||
system_prompt: '', provider: 'siliconflow', model: 'deepseek-ai/DeepSeek-V4-Flash',
|
||||
features: ['chat'] as string[], version: '1.0.0',
|
||||
workflow_ids: [] as string[], agent_ids: [] as string[], knowledge_base_ids: [] as string[],
|
||||
});
|
||||
|
||||
// License form
|
||||
const [licForm, setLicForm] = useState({
|
||||
org_name: '', licensee_email: '', max_users: 10, max_executions: 10000,
|
||||
expiry_date: '', features: ['chat'] as string[],
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [s, d, t] = await Promise.all([
|
||||
adminAPI.getDistributionStats(),
|
||||
adminAPI.getDistributions({ page, page_size: 20 }),
|
||||
adminAPI.getDistributionTemplates(),
|
||||
]);
|
||||
if (s.code === 0) setStats(s.data);
|
||||
if (d.code === 0) { setDistributions(d.data.items); setTotalPages(Math.ceil(d.data.total / 20)); }
|
||||
if (t.code === 0) setTemplates(t.data);
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const loadLicenses = async () => {
|
||||
try {
|
||||
const res = await adminAPI.getDistributionLicenses({ page: licPage, page_size: 20 });
|
||||
if (res.code === 0) { setLicenses(res.data.items); setLicTotalPages(Math.ceil(res.data.total / 20)); }
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
useEffect(() => { loadData(); }, [page]);
|
||||
useEffect(() => { if (tab === 'licenses') loadLicenses(); }, [tab, licPage]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const res = await adminAPI.createDistribution(form);
|
||||
if (res.code === 0) { setShowCreate(false); resetForm(); loadData(); }
|
||||
else alert('创建失败: ' + (res.message || ''));
|
||||
} catch (e) { alert('创建失败'); }
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!editDistro) return;
|
||||
try {
|
||||
const res = await adminAPI.updateDistribution(editDistro.id, form);
|
||||
if (res.code === 0) { setEditDistro(null); resetForm(); loadData(); }
|
||||
else alert('更新失败');
|
||||
} catch (e) { alert('更新失败'); }
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('确定删除此发行版?关联的许可证也将被删除。')) return;
|
||||
try {
|
||||
await adminAPI.deleteDistribution(id);
|
||||
loadData();
|
||||
} catch (e) { alert('删除失败'); }
|
||||
};
|
||||
|
||||
const handleBuild = async (id: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token') || '';
|
||||
window.open(`/app/api/admin/distributions/${id}/build`, '_blank');
|
||||
} catch (e) { alert('下载失败'); }
|
||||
};
|
||||
|
||||
const handleGenerateLicense = async () => {
|
||||
if (!selectedDistro) return;
|
||||
try {
|
||||
const res = await adminAPI.createDistributionLicense(selectedDistro, licForm);
|
||||
if (res.code === 0) { setShowLicense(false); setLicForm({ org_name: '', licensee_email: '', max_users: 10, max_executions: 10000, expiry_date: '', features: ['chat'] }); loadLicenses(); }
|
||||
else alert('生成失败: ' + (res.message || ''));
|
||||
} catch (e) { alert('生成失败'); }
|
||||
};
|
||||
|
||||
const handleRevokeLicense = async (id: string) => {
|
||||
if (!confirm('确定吊销此许可证?')) return;
|
||||
try {
|
||||
await adminAPI.revokeDistributionLicense(id);
|
||||
loadLicenses();
|
||||
} catch (e) { alert('吊销失败'); }
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setForm({ name: '', description: '', brand_name: '', brand_color: '#6366f1', system_prompt: '', provider: 'siliconflow', model: 'deepseek-ai/DeepSeek-V4-Flash', features: ['chat'], version: '1.0.0', workflow_ids: [], agent_ids: [], knowledge_base_ids: [] });
|
||||
};
|
||||
|
||||
const openEdit = (d: Distribution) => {
|
||||
setEditDistro(d);
|
||||
setForm({
|
||||
name: d.name, description: d.description || '', brand_name: d.brand_name || '',
|
||||
brand_color: d.brand_color || '#6366f1', system_prompt: d.system_prompt || '',
|
||||
provider: d.provider || 'siliconflow', model: d.model || 'deepseek-ai/DeepSeek-V4-Flash',
|
||||
features: d.features || ['chat'], version: d.version || '1.0.0',
|
||||
workflow_ids: d.workflow_ids || [], agent_ids: d.agent_ids || [], knowledge_base_ids: d.knowledge_base_ids || [],
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFeature = (feat: string) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
features: prev.features.includes(feat) ? prev.features.filter(f => f !== feat) : [...prev.features, feat],
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleLicFeature = (feat: string) => {
|
||||
setLicForm(prev => ({
|
||||
...prev,
|
||||
features: prev.features.includes(feat) ? prev.features.filter(f => f !== feat) : [...prev.features, feat],
|
||||
}));
|
||||
};
|
||||
|
||||
const isEditing = !!editDistro;
|
||||
|
||||
// Styles
|
||||
const dark = { bg: '#0d1117', card: '#161b22', border: '#21262d', text: '#c9d1d9', sub: '#8b949e', accent: '#6366f1', accentLight: '#818cf8', green: '#3fb950', red: '#f85149', yellow: '#d29922' };
|
||||
|
||||
const cardStyle: React.CSSProperties = { background: dark.card, border: `1px solid ${dark.border}`, borderRadius: 12, padding: 20 };
|
||||
const btnPrimary: React.CSSProperties = { background: dark.accent, color: '#fff', border: 'none', borderRadius: 8, padding: '8px 20px', cursor: 'pointer', fontSize: 14, fontWeight: 600 };
|
||||
const btnDanger: React.CSSProperties = { background: dark.red, color: '#fff', border: 'none', borderRadius: 8, padding: '8px 20px', cursor: 'pointer', fontSize: 14, fontWeight: 600 };
|
||||
const btnGhost: React.CSSProperties = { background: 'transparent', color: dark.text, border: `1px solid ${dark.border}`, borderRadius: 8, padding: '8px 20px', cursor: 'pointer', fontSize: 14 };
|
||||
const inputStyle: React.CSSProperties = { background: '#0d1117', color: dark.text, border: `1px solid ${dark.border}`, borderRadius: 6, padding: '8px 12px', fontSize: 14, width: '100%', boxSizing: 'border-box', outline: 'none' };
|
||||
const textareaStyle: React.CSSProperties = { ...inputStyle, minHeight: 80, resize: 'vertical', fontFamily: 'monospace' };
|
||||
const labelStyle: React.CSSProperties = { color: dark.sub, fontSize: 13, marginBottom: 4, display: 'block' };
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, background: dark.bg, minHeight: '100vh', color: dark.text, fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif' }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
|
||||
<div>
|
||||
<h1 style={{ margin: 0, fontSize: 28, fontWeight: 700 }}>发行版构建器</h1>
|
||||
<p style={{ margin: '4px 0 0', color: dark.sub, fontSize: 13 }}>SaaS 产品线管理 · 自定义发行版配置包生成 · 许可证管理</p>
|
||||
</div>
|
||||
<button onClick={() => { setShowCreate(true); setEditDistro(null); resetForm(); }} style={btnPrimary}>
|
||||
+ 新建发行版
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
{[
|
||||
{ label: '发行版总数', value: stats.total },
|
||||
{ label: '已发布', value: stats.active, color: dark.green },
|
||||
{ label: '累计下载', value: stats.total_downloads, color: dark.accentLight },
|
||||
{ label: '有效许可证', value: stats.active_licenses, color: dark.yellow },
|
||||
].map((s, i) => (
|
||||
<div key={i} style={cardStyle}>
|
||||
<div style={{ color: dark.sub, fontSize: 13 }}>{s.label}</div>
|
||||
<div style={{ fontSize: 32, fontWeight: 700, color: s.color || dark.text, marginTop: 4 }}>{s.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', gap: 0, marginBottom: 20, borderBottom: `2px solid ${dark.border}` }}>
|
||||
{[
|
||||
{ key: 'distributions', label: '发行版管理' },
|
||||
{ key: 'licenses', label: '许可证管理' },
|
||||
].map(t => (
|
||||
<button key={t.key}
|
||||
onClick={() => setTab(t.key as any)}
|
||||
style={{
|
||||
padding: '10px 24px', background: 'transparent', border: 'none',
|
||||
color: tab === t.key ? dark.accentLight : dark.sub,
|
||||
borderBottom: tab === t.key ? `2px solid ${dark.accent}` : '2px solid transparent',
|
||||
cursor: 'pointer', fontSize: 15, fontWeight: tab === t.key ? 600 : 400,
|
||||
}}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Distributions Tab */}
|
||||
{tab === 'distributions' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{distributions.map(d => (
|
||||
<div key={d.id} style={{ ...cardStyle, display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 10, background: d.brand_color || dark.accent, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, fontWeight: 700, color: '#fff', flexShrink: 0 }}>
|
||||
{(d.brand_name || d.name)[0]}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 16, fontWeight: 600 }}>{d.name}</span>
|
||||
<span style={{ fontSize: 11, padding: '2px 8px', borderRadius: 10, background: STATUS_COLORS[d.status] + '22', color: STATUS_COLORS[d.status], fontWeight: 600 }}>
|
||||
{STATUS_LABELS[d.status] || d.status}
|
||||
</span>
|
||||
<span style={{ color: dark.sub, fontSize: 12 }}>v{d.version}</span>
|
||||
</div>
|
||||
<div style={{ color: dark.sub, fontSize: 13, marginTop: 2 }}>{d.description || '无描述'}</div>
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 6, fontSize: 12, color: dark.sub }}>
|
||||
{d.features?.map(f => (
|
||||
<span key={f}>✓ {FEATURE_LABELS[f] || f}</span>
|
||||
))}
|
||||
<span>下载 {d.download_count || 0} 次</span>
|
||||
<span>{d.license_count || 0} 个许可证</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<button onClick={() => handleBuild(d.id)} style={btnGhost}>下载配置包</button>
|
||||
<button onClick={() => { setSelectedDistro(d.id); setShowLicense(true); }} style={btnGhost}>生成许可证</button>
|
||||
<button onClick={() => openEdit(d)} style={btnGhost}>编辑</button>
|
||||
<button onClick={() => handleDelete(d.id)} style={btnDanger}>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 20 }}>
|
||||
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1} style={btnGhost}>上一页</button>
|
||||
<span style={{ padding: '8px 16px', color: dark.sub }}>第 {page}/{totalPages} 页</span>
|
||||
<button onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={page >= totalPages} style={btnGhost}>下一页</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Licenses Tab */}
|
||||
{tab === 'licenses' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
|
||||
<button onClick={() => setShowLicense(true)} style={btnPrimary}>+ 生成许可证</button>
|
||||
</div>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: `1px solid ${dark.border}` }}>
|
||||
{['许可证密钥', '组织', '发行版', '用量', '状态', '过期', '操作'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 12px', textAlign: 'left', color: dark.sub, fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{licenses.map(lic => (
|
||||
<tr key={lic.id} style={{ borderBottom: `1px solid ${dark.border}` }}>
|
||||
<td style={{ padding: '10px 12px', fontFamily: 'monospace', fontSize: 12, color: dark.accentLight }}>{lic.license_key}</td>
|
||||
<td style={{ padding: '10px 12px' }}>{lic.org_name}</td>
|
||||
<td style={{ padding: '10px 12px' }}>{lic.distribution_name}</td>
|
||||
<td style={{ padding: '10px 12px', color: dark.sub }}>{lic.usage_count}/{lic.max_executions}</td>
|
||||
<td style={{ padding: '10px 12px' }}>
|
||||
<span style={{ fontSize: 11, padding: '2px 8px', borderRadius: 10, background: lic.status === 'active' ? '#3fb95022' : '#f8514922', color: lic.status === 'active' ? dark.green : dark.red }}>{lic.status}</span>
|
||||
</td>
|
||||
<td style={{ padding: '10px 12px', color: dark.sub, fontSize: 12 }}>{lic.expiry_date || '永久'}</td>
|
||||
<td style={{ padding: '10px 12px' }}>
|
||||
{lic.status === 'active' && (
|
||||
<button onClick={() => handleRevokeLicense(lic.id)} style={{ ...btnDanger, padding: '4px 12px', fontSize: 12 }}>吊销</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{licTotalPages > 1 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 20 }}>
|
||||
<button onClick={() => setLicPage(p => Math.max(1, p - 1))} disabled={licPage <= 1} style={btnGhost}>上一页</button>
|
||||
<span style={{ padding: '8px 16px', color: dark.sub }}>第 {licPage}/{licTotalPages} 页</span>
|
||||
<button onClick={() => setLicPage(p => Math.min(licTotalPages, p + 1))} disabled={licPage >= licTotalPages} style={btnGhost}>下一页</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
{(showCreate || isEditing) && (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||
<div style={{ ...cardStyle, width: 640, maxHeight: '85vh', overflow: 'auto', padding: 28 }}>
|
||||
<h2 style={{ margin: '0 0 20px' }}>{isEditing ? '编辑发行版' : '新建发行版'}</h2>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>名称 *</label>
|
||||
<input style={inputStyle} value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="例如: Enterprise AI Suite" />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>描述</label>
|
||||
<textarea style={textareaStyle} value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} placeholder="发行版描述..." />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
|
||||
<div>
|
||||
<label style={labelStyle}>品牌名称</label>
|
||||
<input style={inputStyle} value={form.brand_name} onChange={e => setForm({ ...form, brand_name: e.target.value })} placeholder="例如: MyAI" />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>品牌主色</label>
|
||||
<input type="color" style={{ ...inputStyle, height: 38, padding: 4, width: 60 }} value={form.brand_color} onChange={e => setForm({ ...form, brand_color: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>系统提示词</label>
|
||||
<textarea style={textareaStyle} value={form.system_prompt} onChange={e => setForm({ ...form, system_prompt: e.target.value })} placeholder="You are a helpful AI assistant..." />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
|
||||
<div>
|
||||
<label style={labelStyle}>LLM 提供商</label>
|
||||
<select style={inputStyle} value={form.provider} onChange={e => setForm({ ...form, provider: e.target.value })}>
|
||||
<option value="siliconflow">SiliconFlow</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>默认模型</label>
|
||||
<input style={inputStyle} value={form.model} onChange={e => setForm({ ...form, model: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>版本号</label>
|
||||
<input style={inputStyle} value={form.version} onChange={e => setForm({ ...form, version: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={labelStyle}>功能特性</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{Object.entries(FEATURE_LABELS).map(([key, label]) => (
|
||||
<button key={key}
|
||||
onClick={() => toggleFeature(key)}
|
||||
style={{
|
||||
padding: '6px 14px', borderRadius: 20, fontSize: 13, cursor: 'pointer',
|
||||
background: form.features.includes(key) ? dark.accent : 'transparent',
|
||||
color: form.features.includes(key) ? '#fff' : dark.sub,
|
||||
border: form.features.includes(key) ? `1px solid ${dark.accent}` : `1px solid ${dark.border}`,
|
||||
}}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
|
||||
<button onClick={() => { setShowCreate(false); setEditDistro(null); }} style={btnGhost}>取消</button>
|
||||
<button onClick={isEditing ? handleUpdate : handleCreate} style={btnPrimary}>{isEditing ? '保存修改' : '创建'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* License Modal */}
|
||||
{showLicense && (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||
<div style={{ ...cardStyle, width: 500, padding: 28 }}>
|
||||
<h2 style={{ margin: '0 0 20px' }}>生成许可证</h2>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>发行版 *</label>
|
||||
<select style={inputStyle} value={selectedDistro} onChange={e => setSelectedDistro(e.target.value)}>
|
||||
<option value="">-- 选择发行版 --</option>
|
||||
{distributions.map(d => (
|
||||
<option key={d.id} value={d.id}>{d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>组织名称 *</label>
|
||||
<input style={inputStyle} value={licForm.org_name} onChange={e => setLicForm({ ...licForm, org_name: e.target.value })} placeholder="例如: Acme Corp" />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>被授权邮箱</label>
|
||||
<input style={inputStyle} value={licForm.licensee_email} onChange={e => setLicForm({ ...licForm, licensee_email: e.target.value })} placeholder="admin@example.com" />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
|
||||
<div>
|
||||
<label style={labelStyle}>最大用户数</label>
|
||||
<input type="number" style={inputStyle} value={licForm.max_users} onChange={e => setLicForm({ ...licForm, max_users: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>最大执行次数</label>
|
||||
<input type="number" style={inputStyle} value={licForm.max_executions} onChange={e => setLicForm({ ...licForm, max_executions: Number(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={labelStyle}>过期日期 (留空为永久)</label>
|
||||
<input type="date" style={inputStyle} value={licForm.expiry_date} onChange={e => setLicForm({ ...licForm, expiry_date: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={labelStyle}>授权功能</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{Object.entries(FEATURE_LABELS).map(([key, label]) => (
|
||||
<button key={key}
|
||||
onClick={() => toggleLicFeature(key)}
|
||||
style={{
|
||||
padding: '6px 14px', borderRadius: 20, fontSize: 13, cursor: 'pointer',
|
||||
background: licForm.features.includes(key) ? dark.accent : 'transparent',
|
||||
color: licForm.features.includes(key) ? '#fff' : dark.sub,
|
||||
border: licForm.features.includes(key) ? `1px solid ${dark.accent}` : `1px solid ${dark.border}`,
|
||||
}}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
|
||||
<button onClick={() => setShowLicense(false)} style={btnGhost}>取消</button>
|
||||
<button onClick={handleGenerateLicense} style={btnPrimary}>生成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user