feat(models): 桌面端模型卡紧凑化+一行多张(自适应网格)

- 模型卡: 缩内边距/高度, 加彩色首字母图标+名称+id, 能力徽标, 紧凑底部(启用/管理)
- 容器改 CSS Grid auto-fill(minmax 160px), 一行自适应多张, 去掉固定宽
- 解决卡片大而内容小/单行一张
This commit is contained in:
Pine
2026-08-25 17:30:15 +08:00
parent 729c7d86dc
commit 5d5f4b07ff
@@ -20,6 +20,21 @@ interface Props {
onOpenModels: (provider: ProviderInfo) => void;
}
// 图标主题色(按模型 id 首字符给一个稳定渐变色)
const PALETTE = [
"linear-gradient(135deg,#6366f1,#8b5cf6)",
"linear-gradient(135deg,#0ea5e9,#22d3ee)",
"linear-gradient(135deg,#f59e0b,#f97316)",
"linear-gradient(135deg,#10b981,#34d399)",
"linear-gradient(135deg,#ef4444,#f87171)",
"linear-gradient(135deg,#ec4899,#f472b6)",
];
function iconBg(id: string) {
let h = 0;
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
return PALETTE[h % PALETTE.length];
}
export const PineAgentsModelCards = React.memo(function PineAgentsModelCards({
providers,
onOpenModels,
@@ -44,13 +59,13 @@ export const PineAgentsModelCards = React.memo(function PineAgentsModelCards({
try { localStorage.setItem(LS_KEY, JSON.stringify(next)); } catch { /* ignore */ }
};
const tags = (m: ModelInfo) => {
const tgs: string[] = [];
if (m.supports_multimodal) tgs.push(t("models.tagMultimodal", "多模态"));
if (m.supports_image) tgs.push(t("models.tagVision", "视觉"));
if (m.supports_video) tgs.push(t("models.tagVideo", "视频"));
if (tgs.length === 0) tgs.push(t("models.tagText", "文本"));
return tgs;
const chips = (m: ModelInfo) => {
const c: string[] = [];
if (m.supports_multimodal) c.push(t("models.tagMultimodal", "多模态"));
if (m.supports_image) c.push(t("models.tagVision", "视觉"));
if (m.supports_video) c.push(t("models.tagVideo", "视频"));
if (c.length === 0) c.push(t("models.tagText", "文本"));
return c;
};
if (models.length === 0) {
@@ -58,37 +73,37 @@ export const PineAgentsModelCards = React.memo(function PineAgentsModelCards({
}
return (
<div className={styles.providerCards}>
<div className={styles.providerCards} style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: 10 }}>
{models.map(({ provider, model }) => {
const pid = provider.id, mid = model.id;
return (
<div key={keyOf(pid, mid)} className={styles.groupCardGlass} style={{ padding: 14, minWidth: 220 }}>
<div className={styles.groupCardHeader}>
<span className={styles.groupCardName}>{model.name || model.id}</span>
{isEnabled(pid, mid) && (
<span className={styles.groupCardLiveBadge}><span className={styles.groupCardPulse} />Live</span>
)}
</div>
<div className={styles.groupCardContent}>
<div className={styles.groupCardField}>
<span className={styles.groupCardFieldValue}>
<code>{model.id}</code>
</span>
<div className={styles.groupCardFieldValue}>
{tags(model).map((tg) => (
<span key={tg} className={styles.customTag} style={{ marginRight: 4 }}>{tg}</span>
))}
<div key={keyOf(pid, mid)}
className={styles.groupCardGlass}
style={{ padding: 10, border: "1px solid var(--ant-color-border)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ width: 30, height: 30, borderRadius: 7, display: "inline-flex", alignItems: "center",
justifyContent: "center", background: iconBg(mid), color: "#fff", fontWeight: 700, fontSize: 15, flexShrink: 0 }}>
{(model.name || mid || "?").charAt(0).toUpperCase()}
</span>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 13, lineHeight: 1.2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{model.name || mid}
</div>
<code style={{ fontSize: 11, color: "var(--ant-color-text-secondary)", display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{mid}</code>
</div>
{isEnabled(pid, mid) && <span className={styles.groupCardLiveBadge} style={{ flexShrink: 0 }}><span className={styles.groupCardPulse} />Live</span>}
</div>
<div className={styles.groupCardActions}>
<label className={styles.groupCardField} style={{ alignItems: "center", gap: 8 }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 8 }}>
{chips(model).map((tg) => (
<span key={tg} className={styles.customTag} style={{ fontSize: 11, padding: "1px 7px", borderRadius: 999, background: "var(--ant-color-fill)" }}>{tg}</span>
))}
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 8, paddingTop: 8, borderTop: "1px solid var(--ant-color-border)" }}>
<label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12 }}>
<Switch checked={isEnabled(pid, mid)} onChange={(v) => toggle(pid, mid, !!v)} />
{isEnabled(pid, mid) ? t("models.enabled", "启用") : t("models.disabled", "禁用")}
</label>
<button className={styles.groupCardActBtn} onClick={() => onOpenModels(provider)}>
{t("models.settings", "管理")}
</button>
<button className={styles.groupCardActBtn} onClick={() => onOpenModels(provider)} style={{ fontSize: 12 }}>{t("models.settings", "管理")}</button>
</div>
</div>
);