feat(s2a): Improve group display layout with grid-based cards
- Replace badge-style group display with grid-based card layout for better readability - Add group name and ID display in selected groups section with truncation handling - Reorganize profile card group summary to use dedicated grid section below parameters - Remove inline group information from parameter summary badge - Improve visual hierarchy and spacing in group display components - Enhance dark mode styling for group cards with better contrast and borders
This commit is contained in:
@@ -420,9 +420,8 @@ export default function S2AConfig() {
|
|||||||
<Input label="默认优先级" type="number" min={0} max={100} value={formPriority} onChange={(e) => setFormPriority(Number(e.target.value))} hint="数值越大优先级越高" />
|
<Input label="默认优先级" type="number" min={0} max={100} value={formPriority} onChange={(e) => setFormPriority(Number(e.target.value))} hint="数值越大优先级越高" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 标题 + 获取按钮 */}
|
{/* 获取按钮 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-end">
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">分组</label>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleFetchGroups}
|
onClick={handleFetchGroups}
|
||||||
@@ -437,15 +436,21 @@ export default function S2AConfig() {
|
|||||||
<p className="text-xs text-slate-400">请先填写 S2A API 地址和 Admin Key,再获取分组列表</p>
|
<p className="text-xs text-slate-400">请先填写 S2A API 地址和 Admin Key,再获取分组列表</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* 已选分组 badges */}
|
{/* 已选分组 */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
{formGroupIds.map(id => (
|
{formGroupIds.map(id => {
|
||||||
<span key={id} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
|
const group = availableGroups.find(g => g.id === id)
|
||||||
{getGroupLabel(id)}
|
return (
|
||||||
<button onClick={() => handleRemoveGroupId(id)} className="hover:text-red-500 transition-colors"><X className="h-3 w-3" /></button>
|
<div key={id} className="flex items-center justify-between px-3 py-2 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800/40">
|
||||||
</span>
|
<div className="flex flex-col min-w-0">
|
||||||
))}
|
<span className="text-sm font-medium text-blue-700 dark:text-blue-300 truncate">{group ? group.name : `#${id}`}</span>
|
||||||
{formGroupIds.length === 0 && <span className="text-sm text-slate-400">未选择分组</span>}
|
{group && <span className="text-[10px] text-blue-400 dark:text-blue-500">#{id}</span>}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => handleRemoveGroupId(id)} className="ml-2 text-blue-400 hover:text-red-500 transition-colors flex-shrink-0"><X className="h-3.5 w-3.5" /></button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{formGroupIds.length === 0 && <span className="text-sm text-slate-400 col-span-full">未选择分组</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 可选分组列表 */}
|
{/* 可选分组列表 */}
|
||||||
@@ -621,18 +626,13 @@ function ProfileCard({ profile, isActive, isActivating, groupNameCache, onActiva
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 参数摘要 */}
|
{/* 参数摘要 */}
|
||||||
<div className="flex flex-wrap gap-1.5 mb-4">
|
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300">
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300">
|
||||||
并发 {profile.concurrency}
|
并发 {profile.concurrency}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300">
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300">
|
||||||
优先级 {profile.priority}
|
优先级 {profile.priority}
|
||||||
</span>
|
</span>
|
||||||
{parsedGroups.length > 0 && (
|
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">
|
|
||||||
分组 {parsedGroups.map(id => groupNameCache[id] ? `${groupNameCache[id]} #${id}` : `#${id}`).join(', ')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{profile.proxy_enabled && (
|
{profile.proxy_enabled && (
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-orange-50 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400">
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-orange-50 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400">
|
||||||
<Globe className="h-3 w-3 mr-0.5" /> 代理
|
<Globe className="h-3 w-3 mr-0.5" /> 代理
|
||||||
@@ -640,6 +640,18 @@ function ProfileCard({ profile, isActive, isActivating, groupNameCache, onActiva
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 分组列表 */}
|
||||||
|
{parsedGroups.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||||
|
{parsedGroups.map(id => (
|
||||||
|
<div key={id} className="flex flex-col px-2.5 py-1.5 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800/40">
|
||||||
|
<span className="text-xs font-medium text-blue-700 dark:text-blue-300 truncate">{groupNameCache[id] || `#${id}`}</span>
|
||||||
|
{groupNameCache[id] && <span className="text-[10px] text-blue-400 dark:text-blue-500">#{id}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!isActive ? (
|
{!isActive ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user