小优化,产品愿景,视觉规范,交互和导航大版本

This commit is contained in:
Nostars Developer
2026-04-16 18:11:35 +08:00
parent 5878f7d9f4
commit 10f9c0061a
51 changed files with 5010 additions and 847 deletions

View File

@@ -1,8 +1,10 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import type { ImageModelInfo } from "@/lib/types";
import { fetchModels } from "@/lib/api";
import { slideUp } from "@/components/ui/motion-presets";
import { getStoreUserId } from "@/lib/store";
@@ -61,7 +63,7 @@ export function ModelSelector({ value, onChange }: ModelSelectorProps) {
border border-[var(--border)] bg-[var(--bg-tertiary)]
text-[var(--text-secondary)] hover:text-[var(--accent)]
hover:border-[var(--accent)]/40 hover:bg-[var(--accent)]/5
transition-all cursor-pointer"
transition-all cursor-pointer btn-hover-lift"
title="切换生图模型"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -79,49 +81,56 @@ export function ModelSelector({ value, onChange }: ModelSelectorProps) {
</svg>
</button>
{open && (
<div
className="absolute bottom-full left-0 mb-1.5 w-56 rounded-xl
glass-panel shadow-xl shadow-black/40 overflow-hidden z-50"
>
{models.map((m) => {
const isActive = m.id === value;
return (
<button
key={m.id}
onClick={() => {
onChange(m.id);
localStorage.setItem(getModelStorageKey(), m.id);
setOpen(false);
}}
className={`w-full text-left px-3.5 py-2.5 flex flex-col gap-0.5
transition-all cursor-pointer
${isActive
? "bg-[var(--accent)]/8 text-[var(--accent)]"
: "text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
}`}
>
<span className="text-sm font-medium flex items-center gap-1.5">
{m.name}
{m.supports_ref_image && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full
bg-[var(--accent)]/10 text-[var(--accent)] font-medium leading-none
border border-[var(--accent)]/20">
</span>
)}
{isActive && (
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
)}
</span>
<span className="text-xs text-[var(--text-secondary)]">{m.description}</span>
</button>
);
})}
</div>
)}
<AnimatePresence>
{open && (
<motion.div
variants={slideUp}
initial="hidden"
animate="visible"
exit={{ opacity: 0, y: 4, transition: { duration: 0.1 } }}
className="absolute bottom-full left-0 mb-1.5 w-56 rounded-xl
surface-3 overflow-hidden z-50"
>
{models.map((m) => {
const isActive = m.id === value;
return (
<button
key={m.id}
onClick={() => {
onChange(m.id);
localStorage.setItem(getModelStorageKey(), m.id);
setOpen(false);
}}
className={`w-full text-left px-3.5 py-2.5 flex flex-col gap-0.5
transition-all cursor-pointer
${isActive
? "bg-[var(--accent)]/8 text-[var(--accent)]"
: "text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
}`}
>
<span className="text-sm font-medium flex items-center gap-1.5">
{m.name}
{m.supports_ref_image && (
<span className="typo-micro px-1.5 py-0.5 rounded-full
bg-[var(--accent)]/10 text-[var(--accent)] leading-none
border border-[var(--accent)]/20"
style={{ textTransform: "none", fontSize: "10px" }}>
</span>
)}
{isActive && (
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
)}
</span>
<span className="typo-caption">{m.description}</span>
</button>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
);
}