加了一堆模型和一堆功能
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useApp } from "@/lib/app-context";
|
||||
import { getImageUrl } from "@/lib/api";
|
||||
import { getImageUrl, fetchLlmModels } from "@/lib/api";
|
||||
import type { LlmModelInfo } from "@/lib/types";
|
||||
|
||||
function timeAgo(ts: number): string {
|
||||
const diff = Date.now() - ts;
|
||||
@@ -20,41 +21,68 @@ export function SessionList() {
|
||||
const {
|
||||
sessions, activeSessionId, tags, selectedTagIds,
|
||||
createSession, switchSession, deleteSession, renameSession,
|
||||
updateSessionLlmModel,
|
||||
} = useApp();
|
||||
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; sessionId: string } | null>(null);
|
||||
const [menuSessionId, setMenuSessionId] = useState<string | null>(null);
|
||||
const [showModelSub, setShowModelSub] = useState(false);
|
||||
const [llmModels, setLlmModels] = useState<LlmModelInfo[]>([]);
|
||||
const [defaultLlmModel, setDefaultLlmModel] = useState("");
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editTitle, setEditTitle] = useState("");
|
||||
const editRef = useRef<HTMLInputElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 标签过滤
|
||||
const filtered = selectedTagIds.length === 0
|
||||
? sessions
|
||||
: sessions.filter((s) => selectedTagIds.some((tid) => s.tags.includes(tid)));
|
||||
|
||||
const tagMap = new Map(tags.map((t) => [t.id, t]));
|
||||
|
||||
// 关闭右键菜单
|
||||
useEffect(() => {
|
||||
const close = () => setContextMenu(null);
|
||||
window.addEventListener("click", close);
|
||||
return () => window.removeEventListener("click", close);
|
||||
fetchLlmModels()
|
||||
.then((data) => {
|
||||
setLlmModels(data.models);
|
||||
setDefaultLlmModel(data.default);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// 编辑聚焦
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
if (menuSessionId) {
|
||||
window.addEventListener("mousedown", handler);
|
||||
return () => window.removeEventListener("mousedown", handler);
|
||||
}
|
||||
}, [menuSessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingId) editRef.current?.focus();
|
||||
}, [editingId]);
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent, sessionId: string) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, sessionId });
|
||||
const closeMenu = useCallback(() => {
|
||||
setMenuSessionId(null);
|
||||
setShowModelSub(false);
|
||||
}, []);
|
||||
|
||||
const handleDots = (e: React.MouseEvent, sessionId: string) => {
|
||||
e.stopPropagation();
|
||||
if (menuSessionId === sessionId) {
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
setMenuSessionId(sessionId);
|
||||
setShowModelSub(false);
|
||||
};
|
||||
|
||||
const startRename = (id: string, currentTitle: string) => {
|
||||
setEditingId(id);
|
||||
setEditTitle(currentTitle);
|
||||
setContextMenu(null);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const commitRename = () => {
|
||||
@@ -64,16 +92,26 @@ export function SessionList() {
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleSelectModel = (sessionId: string, modelId: string) => {
|
||||
updateSessionLlmModel(sessionId, modelId);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const menuSession = menuSessionId
|
||||
? sessions.find((s) => s.id === menuSessionId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto fog-scroll">
|
||||
{/* 新建按钮 */}
|
||||
<div className="px-3 py-2">
|
||||
<div className="px-3 py-3">
|
||||
<button
|
||||
onClick={() => createSession()}
|
||||
className="w-full flex items-center justify-center gap-1.5 px-3 py-2
|
||||
text-sm rounded-lg border border-dashed border-[var(--border)]
|
||||
text-[var(--text-secondary)] hover:border-[var(--accent)]
|
||||
hover:text-[var(--accent)] transition-colors cursor-pointer"
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2.5
|
||||
text-sm rounded-xl border border-dashed border-[var(--accent)]/30
|
||||
text-[var(--accent)]/70 hover:border-[var(--accent)]
|
||||
hover:text-[var(--accent)] hover:bg-[var(--accent)]/5
|
||||
transition-all cursor-pointer"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
@@ -83,114 +121,199 @@ export function SessionList() {
|
||||
</div>
|
||||
|
||||
{/* 会话列表 */}
|
||||
<div className="px-2 pb-2 space-y-0.5">
|
||||
<div className="px-2 pb-2 space-y-1">
|
||||
{filtered.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => switchSession(session.id)}
|
||||
onContextMenu={(e) => handleContextMenu(e, session.id)}
|
||||
className={`group relative flex items-start gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer
|
||||
transition-colors ${
|
||||
session.id === activeSessionId
|
||||
? "bg-[var(--bg-tertiary)]"
|
||||
: "hover:bg-[var(--bg-tertiary)]/50"
|
||||
}`}
|
||||
>
|
||||
{/* 缩略图 */}
|
||||
{session.thumbnail ? (
|
||||
<img
|
||||
src={getImageUrl(session.thumbnail)}
|
||||
alt=""
|
||||
className="w-9 h-9 rounded object-cover flex-shrink-0 border border-[var(--border)]"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
flex items-center justify-center flex-shrink-0 text-[var(--text-secondary)] text-xs">
|
||||
💬
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 会话信息 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{editingId === session.id ? (
|
||||
<input
|
||||
ref={editRef}
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
onBlur={commitRename}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") commitRename();
|
||||
if (e.key === "Escape") setEditingId(null);
|
||||
}}
|
||||
className="w-full text-sm bg-transparent border-b border-[var(--accent)]
|
||||
text-[var(--text-primary)] focus:outline-none"
|
||||
<div key={session.id} className="relative">
|
||||
<div
|
||||
onClick={() => switchSession(session.id)}
|
||||
className={`group relative flex items-start gap-3 px-3 py-2.5 rounded-xl cursor-pointer
|
||||
transition-all ${
|
||||
session.id === activeSessionId
|
||||
? "glow-border !bg-[var(--bg-secondary)]"
|
||||
: "hover:bg-[var(--bg-tertiary)]/50 border border-transparent"
|
||||
}`}
|
||||
>
|
||||
{/* 缩略图 */}
|
||||
{session.thumbnail ? (
|
||||
<img
|
||||
src={getImageUrl(session.thumbnail)}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-lg object-cover flex-shrink-0 border border-[var(--border)]
|
||||
shadow-[0_0_8px_rgba(77,184,164,0.08)]"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-[var(--text-primary)] truncate">
|
||||
{session.title}
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--bg-primary)] border border-[var(--border)]
|
||||
flex items-center justify-center flex-shrink-0">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" strokeWidth="1.5">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签 + 时间 */}
|
||||
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||
{session.tags.slice(0, 3).map((tid) => {
|
||||
const tag = tagMap.get(tid);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<span
|
||||
key={tid}
|
||||
className="text-[10px] px-1.5 py-px rounded-full"
|
||||
style={{ backgroundColor: tag.color + "22", color: tag.color }}
|
||||
>
|
||||
{tag.name}
|
||||
{/* 会话信息 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{editingId === session.id ? (
|
||||
<input
|
||||
ref={editRef}
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
onBlur={commitRename}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") commitRename();
|
||||
if (e.key === "Escape") setEditingId(null);
|
||||
}}
|
||||
className="w-full text-sm bg-transparent border-b border-[var(--accent)]
|
||||
text-[var(--text-primary)] focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<div className={`text-sm truncate pr-5 ${
|
||||
session.id === activeSessionId ? "text-[var(--text-primary)] font-medium" : "text-[var(--text-primary)]"
|
||||
}`}>
|
||||
{session.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签 + 时间 */}
|
||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||
{session.tags.slice(0, 3).map((tid) => {
|
||||
const tag = tagMap.get(tid);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<span
|
||||
key={tid}
|
||||
className="text-[10px] px-1.5 py-px rounded-full font-medium"
|
||||
style={{ backgroundColor: tag.color + "22", color: tag.color }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{session.llmModel && (
|
||||
<span className="text-[10px] px-1.5 py-px rounded-full font-medium
|
||||
bg-[var(--accent)]/10 text-[var(--accent)]">
|
||||
{llmModels.find((m) => m.id === session.llmModel)?.name || session.llmModel}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<span className="text-[10px] text-[var(--text-secondary)] ml-auto flex-shrink-0">
|
||||
{timeAgo(session.updatedAt)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-[var(--text-secondary)] ml-auto flex-shrink-0">
|
||||
{timeAgo(session.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 三点按钮 */}
|
||||
<button
|
||||
onClick={(e) => handleDots(e, session.id)}
|
||||
className="absolute right-2 top-2.5 p-1 rounded-md
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
hover:bg-[var(--bg-tertiary)]
|
||||
opacity-0 group-hover:opacity-100 transition-all cursor-pointer"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="5" r="2" />
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
<circle cx="12" cy="19" r="2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 下拉菜单:在会话项正下方展开 */}
|
||||
{menuSessionId === session.id && menuSession && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="mx-1 mt-1 glass-panel rounded-xl shadow-xl shadow-black/30 py-1.5
|
||||
overflow-hidden z-10 relative"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 模型选择(折叠/展开式) */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModelSub(!showModelSub);
|
||||
}}
|
||||
className="w-full text-left px-3.5 py-2 text-sm text-[var(--text-primary)]
|
||||
hover:bg-[var(--bg-tertiary)] cursor-pointer transition-colors
|
||||
flex items-center justify-between"
|
||||
>
|
||||
<span>对话模型</span>
|
||||
<svg
|
||||
width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2"
|
||||
className={`transition-transform ${showModelSub ? "rotate-90" : ""}`}
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showModelSub && (
|
||||
<div className="border-t border-[var(--border)] mx-2 mt-1 pt-1 max-h-[240px] overflow-y-auto">
|
||||
{llmModels.map((model) => {
|
||||
const isActive = menuSession.llmModel
|
||||
? menuSession.llmModel === model.id
|
||||
: model.id === defaultLlmModel;
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectModel(menuSessionId!, model.id);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-1.5 text-sm cursor-pointer transition-colors
|
||||
rounded-lg flex items-center gap-2 ${
|
||||
isActive
|
||||
? "text-[var(--accent)] bg-[var(--accent)]/5"
|
||||
: "text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
||||
}`}
|
||||
>
|
||||
<span className="w-4 flex-shrink-0 text-center">
|
||||
{isActive && (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{model.name}</div>
|
||||
<div className="text-[10px] text-[var(--text-secondary)] truncate">{model.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mx-2 my-1 border-t border-[var(--border)]" />
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (menuSession) startRename(menuSession.id, menuSession.title);
|
||||
}}
|
||||
className="w-full text-left px-3.5 py-2 text-sm text-[var(--text-primary)]
|
||||
hover:bg-[var(--bg-tertiary)] cursor-pointer transition-colors"
|
||||
>
|
||||
重命名
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteSession(menuSessionId!);
|
||||
closeMenu();
|
||||
}}
|
||||
className="w-full text-left px-3.5 py-2 text-sm text-[var(--hot)]
|
||||
hover:bg-[var(--bg-tertiary)] cursor-pointer transition-colors"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="text-center text-xs text-[var(--text-secondary)] py-6">
|
||||
<div className="text-center text-xs text-[var(--text-secondary)] py-8">
|
||||
没有匹配的对话
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="fixed z-50 bg-[var(--bg-secondary)] border border-[var(--border)]
|
||||
rounded-lg shadow-xl py-1 min-w-[120px]"
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
const s = sessions.find((s) => s.id === contextMenu.sessionId);
|
||||
if (s) startRename(s.id, s.title);
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-sm text-[var(--text-primary)]
|
||||
hover:bg-[var(--bg-tertiary)] cursor-pointer"
|
||||
>
|
||||
重命名
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteSession(contextMenu.sessionId);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-sm text-red-400
|
||||
hover:bg-[var(--bg-tertiary)] cursor-pointer"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,30 +12,37 @@ export function Sidebar() {
|
||||
{/* 移动端遮罩 */}
|
||||
{!sidebarCollapsed && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/50 sidebar-overlay md:hidden"
|
||||
className="fixed inset-0 z-30 bg-black/60 sidebar-overlay md:hidden"
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside
|
||||
className={`
|
||||
/* 移动端:固定定位抽屉 */
|
||||
fixed inset-y-0 left-0 z-40 w-[280px]
|
||||
md:relative md:inset-auto md:z-auto
|
||||
flex-shrink-0 border-r border-[var(--border)] bg-[var(--bg-secondary)]
|
||||
flex flex-col transition-transform duration-200
|
||||
flex-shrink-0 border-r border-[var(--border)]
|
||||
bg-[var(--bg-secondary)]/80 backdrop-blur-xl
|
||||
flex flex-col transition-transform duration-250 ease-in-out
|
||||
${sidebarCollapsed ? "-translate-x-full md:-translate-x-0 md:w-0 md:border-r-0" : "translate-x-0 md:w-[280px]"}
|
||||
${sidebarCollapsed ? "md:overflow-hidden" : ""}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-[var(--border)]">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">
|
||||
对话
|
||||
</span>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2"
|
||||
style={{ filter: "drop-shadow(0 0 4px rgba(77, 184, 164, 0.35))" }}>
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
||||
</svg>
|
||||
<span className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
对话
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
transition-colors cursor-pointer p-0.5"
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
transition-colors cursor-pointer p-1 rounded-md
|
||||
hover:bg-[var(--accent)]/5"
|
||||
title="折叠侧边栏"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useApp } from "@/lib/app-context";
|
||||
import type { Tag } from "@/lib/types";
|
||||
|
||||
const TAG_COLORS = [
|
||||
"#6366f1", "#f59e0b", "#10b981", "#ec4899",
|
||||
"#8b5cf6", "#06b6d4", "#f97316", "#84cc16",
|
||||
"#4DB8A4", "#3A8FB7", "#8b5cf6", "#B8935A",
|
||||
"#ec4899", "#C4654A", "#84cc16", "#6366f1",
|
||||
];
|
||||
|
||||
export function TagFilter() {
|
||||
@@ -34,15 +33,14 @@ export function TagFilter() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2 border-b border-[var(--border)]">
|
||||
{/* 标签 pills */}
|
||||
<div className="px-3 py-2.5 border-b border-[var(--border)]">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
onClick={clearFilter}
|
||||
className={`px-2 py-0.5 text-xs rounded-full transition-colors cursor-pointer ${
|
||||
className={`px-2.5 py-1 text-xs rounded-lg transition-all cursor-pointer ${
|
||||
selectedTagIds.length === 0
|
||||
? "bg-[var(--accent)] text-white"
|
||||
: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
? "bg-[var(--accent)]/15 text-[var(--accent)] border border-[var(--accent)]/30"
|
||||
: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] border border-transparent"
|
||||
}`}
|
||||
>
|
||||
全部
|
||||
@@ -53,10 +51,10 @@ export function TagFilter() {
|
||||
<button
|
||||
key={tag.id}
|
||||
onClick={() => toggle(tag.id)}
|
||||
className="px-2 py-0.5 text-xs rounded-full transition-colors cursor-pointer border"
|
||||
className="px-2.5 py-1 text-xs rounded-lg transition-all cursor-pointer border"
|
||||
style={{
|
||||
backgroundColor: active ? tag.color + "22" : "transparent",
|
||||
borderColor: active ? tag.color : "var(--border)",
|
||||
backgroundColor: active ? tag.color + "18" : "transparent",
|
||||
borderColor: active ? tag.color + "60" : "var(--border)",
|
||||
color: active ? tag.color : "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
@@ -66,74 +64,75 @@ export function TagFilter() {
|
||||
})}
|
||||
<button
|
||||
onClick={() => setShowManager(!showManager)}
|
||||
className="px-2 py-0.5 text-xs rounded-full
|
||||
className="px-2 py-1 text-xs rounded-lg
|
||||
text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
transition-colors cursor-pointer"
|
||||
hover:bg-[var(--accent)]/5
|
||||
transition-all cursor-pointer"
|
||||
title="管理标签"
|
||||
>
|
||||
⚙
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 标签管理面板 */}
|
||||
{showManager && (
|
||||
<div className="mt-2 p-2 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border)]">
|
||||
<div className="mt-2.5 p-3 rounded-xl bg-[var(--bg-tertiary)] border border-[var(--border)]">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<input
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleAddTag()}
|
||||
placeholder="新标签名称"
|
||||
className="flex-1 px-2 py-1 text-xs rounded bg-[var(--bg-primary)]
|
||||
className="flex-1 min-w-0 px-2.5 py-1.5 text-xs rounded-lg bg-[var(--bg-primary)]
|
||||
border border-[var(--border)] text-[var(--text-primary)]
|
||||
placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]"
|
||||
focus:outline-none focus:border-[var(--accent)]/50"
|
||||
/>
|
||||
<div className="flex gap-0.5">
|
||||
{TAG_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setNewTagColor(c)}
|
||||
className="w-4 h-4 rounded-full cursor-pointer transition-transform"
|
||||
style={{
|
||||
backgroundColor: c,
|
||||
transform: newTagColor === c ? "scale(1.3)" : "scale(1)",
|
||||
boxShadow: newTagColor === c ? `0 0 0 2px var(--bg-tertiary), 0 0 0 3px ${c}` : "none",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddTag}
|
||||
disabled={!newTagName.trim()}
|
||||
className="px-2 py-1 text-xs rounded bg-[var(--accent)] text-white
|
||||
className="flex-shrink-0 px-2.5 py-1.5 text-xs rounded-lg bg-[var(--accent)] text-[var(--bg-primary)] font-medium
|
||||
disabled:opacity-40 cursor-pointer hover:bg-[var(--accent-hover)]
|
||||
transition-colors"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
{TAG_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setNewTagColor(c)}
|
||||
className="w-5 h-5 rounded-full cursor-pointer transition-transform"
|
||||
style={{
|
||||
backgroundColor: c,
|
||||
transform: newTagColor === c ? "scale(1.25)" : "scale(1)",
|
||||
boxShadow: newTagColor === c ? `0 0 0 2px var(--bg-tertiary), 0 0 0 3px ${c}` : "none",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 自定义标签列表(可删除) */}
|
||||
{tags.filter((t) => !t.builtin).length > 0 && (
|
||||
<div className="space-y-1 mt-1">
|
||||
<div className="space-y-1 mt-1.5">
|
||||
{tags
|
||||
.filter((t) => !t.builtin)
|
||||
.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="flex items-center justify-between px-2 py-0.5 rounded text-xs"
|
||||
className="flex items-center justify-between px-2 py-1 rounded-lg text-xs
|
||||
hover:bg-[var(--bg-primary)]/50 transition-colors"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-2.5 h-2.5 rounded-full"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
/>
|
||||
{tag.name}
|
||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: tag.color }} />
|
||||
<span className="text-[var(--text-primary)]">{tag.name}</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={() => deleteTag(tag.id)}
|
||||
className="text-[var(--text-secondary)] hover:text-red-400 cursor-pointer"
|
||||
className="text-[var(--text-secondary)] hover:text-[var(--hot)] cursor-pointer
|
||||
transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user