diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx index 3fa0d95..14e1523 100644 --- a/app/(admin)/layout.tsx +++ b/app/(admin)/layout.tsx @@ -25,7 +25,12 @@ export default async function AdminLayout({ name: link.name, url: link.url, order: link.order, + toolboxGroup: link.toolboxGroup || 1, }))} + toolbox1Name={siteSettings.toolbox1Name || "工具箱"} + toolbox1Enabled={siteSettings.toolbox1Enabled ?? true} + toolbox2Name={siteSettings.toolbox2Name || "工具箱2"} + toolbox2Enabled={siteSettings.toolbox2Enabled ?? false} />
{children}
diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 70cd9c4..b179b7f 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -155,6 +155,10 @@ type SiteSettings = { bannerEnabled: boolean; bannerText: string; bannerLink: string | null; + toolbox1Name: string; + toolbox1Enabled: boolean; + toolbox2Name: string; + toolbox2Enabled: boolean; }; type NavLink = { @@ -163,6 +167,7 @@ type NavLink = { url: string; order: number; isEnabled: boolean; + toolboxGroup: number; // 1 或 2 }; type DingTalkOrganization = { @@ -348,6 +353,10 @@ export function AdminSettingsClient() { bannerEnabled: true, bannerText: "升级会员, Nano Banana Pro 免费365天!", bannerLink: null, + toolbox1Name: "工具箱", + toolbox1Enabled: true, + toolbox2Name: "工具箱2", + toolbox2Enabled: false, }); useEffect(() => { @@ -366,6 +375,10 @@ export function AdminSettingsClient() { bannerEnabled: siteSettingsData.bannerEnabled ?? true, bannerText: siteSettingsData.bannerText || "升级会员, Nano Banana Pro 免费365天!", bannerLink: siteSettingsData.bannerLink || null, + toolbox1Name: siteSettingsData.toolbox1Name || "工具箱", + toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true, + toolbox2Name: siteSettingsData.toolbox2Name || "工具箱2", + toolbox2Enabled: siteSettingsData.toolbox2Enabled ?? false, }); } }, [siteSettingsData]); @@ -535,6 +548,7 @@ export function AdminSettingsClient() { url: "", order: 0, isEnabled: true, + toolboxGroup: 1, // 默认属于工具箱1 }); const [showNavLinkDialog, setShowNavLinkDialog] = useState(false); const [editingNavLink, setEditingNavLink] = useState(null); @@ -1009,7 +1023,7 @@ export function AdminSettingsClient() { // --- 导航链接逻辑 --- const resetNavLinkForm = () => { - setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true }); + setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true, toolboxGroup: 1 }); setEditingNavLink(null); }; @@ -2471,10 +2485,72 @@ export function AdminSettingsClient() { {/* 导航设置 */} - + + {/* 工具箱名称设置 */} - -
+ + 工具箱设置 + 配置导航栏上的工具箱名称和显示状态 + + + {/* 工具箱1 */} +
+
+
+ + setSiteSettings({ ...siteSettings, toolbox1Name: e.target.value })} + placeholder="工具箱名称" + className="max-w-[200px]" + /> +
+
+ 主工具箱,用于展示常用的外部链接 +
+
+ setSiteSettings({ ...siteSettings, toolbox1Enabled: checked })} + /> +
+ + {/* 工具箱2 */} +
+
+
+ + setSiteSettings({ ...siteSettings, toolbox2Name: e.target.value })} + placeholder="工具箱2名称" + className="max-w-[200px]" + /> +
+
+ 第二个工具箱,可用于分类展示不同类型的链接 +
+
+ setSiteSettings({ ...siteSettings, toolbox2Enabled: checked })} + /> +
+ + +
+ + + {/* 导航链接列表 */} + + +
+
+ 导航链接 + 添加和管理工具箱中的链接 +
- - {editingNavLink ? "编辑链接" : "添加链接"} - -
-
- - setNavLinkForm({ ...navLinkForm, name: e.target.value })} - placeholder="链接名称" - /> -
-
- - setNavLinkForm({ ...navLinkForm, url: e.target.value })} - placeholder="https://..." - /> + + {editingNavLink ? "编辑链接" : "添加链接"} + +
+
+ + setNavLinkForm({ ...navLinkForm, name: e.target.value })} + placeholder="链接名称" + /> +
+
+ + setNavLinkForm({ ...navLinkForm, url: e.target.value })} + placeholder="https://..." + /> +
+
+ +
+ + +
+
+
+ + setNavLinkForm({ ...navLinkForm, order: parseInt(e.target.value) || 0 })} + /> +
+
+ setNavLinkForm({ ...navLinkForm, isEnabled: checked })} + /> + +
-
- - setNavLinkForm({ ...navLinkForm, order: parseInt(e.target.value) || 0 })} - /> -
-
- setNavLinkForm({ ...navLinkForm, isEnabled: checked })} - /> - -
-
- - - - - + + + + +
- +
+ + {/* 工具箱1 的链接 */}
- {navLinksData?.links?.map((link) => ( +
+ {siteSettings.toolbox1Name || "工具箱1"} 的链接 +
+ {navLinksData?.links?.filter(l => (l.toolboxGroup || 1) === 1).map((link) => (
@@ -2547,21 +2655,66 @@ export function AdminSettingsClient() { url: link.url, order: link.order, isEnabled: link.isEnabled, + toolboxGroup: link.toolboxGroup || 1, }); setEditingNavLink(link.id); setShowNavLinkDialog(true); }} > - +
))} - {(!navLinksData?.links?.length) && ( -
暂无导航链接
+ {(!navLinksData?.links?.filter(l => (l.toolboxGroup || 1) === 1).length) && ( +
暂无链接
+ )} +
+ + {/* 工具箱2 的链接 */} +
+
+ {siteSettings.toolbox2Name || "工具箱2"} 的链接 +
+ {navLinksData?.links?.filter(l => l.toolboxGroup === 2).map((link) => ( +
+
+
+ {link.name} + {!link.isEnabled && 已禁用} +
+
{link.url} (排序: {link.order})
+
+
+ + +
+
+ ))} + {(!navLinksData?.links?.filter(l => l.toolboxGroup === 2).length) && ( +
暂无链接
)}
diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 7501c81..842c96e 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -44,8 +44,13 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) { id: link.id, name: link.name, url: link.url, - order: link.order + order: link.order, + toolboxGroup: link.toolboxGroup || 1, }))} + toolbox1Name={siteSettings.toolbox1Name || "工具箱"} + toolbox1Enabled={siteSettings.toolbox1Enabled ?? true} + toolbox2Name={siteSettings.toolbox2Name || "工具箱2"} + toolbox2Enabled={siteSettings.toolbox2Enabled ?? false} />
{/* Add padding-top for fixed navbar */} diff --git a/app/api/nav-links/route.ts b/app/api/nav-links/route.ts index a436798..3e38916 100644 --- a/app/api/nav-links/route.ts +++ b/app/api/nav-links/route.ts @@ -29,6 +29,7 @@ const createSchema = z.object({ url: z.string().url(), order: z.number().int().default(0), isEnabled: z.boolean().default(true), + toolboxGroup: z.number().int().min(1).max(2).default(1), // 属于哪个工具箱 }); // POST /api/nav-links - 创建导航链接(仅管理员) @@ -79,6 +80,7 @@ const updateSchema = z.object({ url: z.string().url().optional(), order: z.number().int().optional(), isEnabled: z.boolean().optional(), + toolboxGroup: z.number().int().min(1).max(2).optional(), // 属于哪个工具箱 }); // PUT /api/nav-links - 更新导航链接(仅管理员) diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index b3812c3..bda1627 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -34,6 +34,10 @@ export async function GET() { bannerEnabled: true, bannerText: "升级会员, Nano Banana Pro 免费365天!", bannerLink: null, + toolbox1Name: "工具箱", + toolbox1Enabled: true, + toolbox2Name: "工具箱2", + toolbox2Enabled: false, }; const response = NextResponse.json({ @@ -70,6 +74,10 @@ const updateSchema = z.object({ bannerEnabled: z.boolean().optional(), bannerText: z.string().optional(), bannerLink: z.string().nullable().optional(), + toolbox1Name: z.string().max(64).optional(), + toolbox1Enabled: z.boolean().optional(), + toolbox2Name: z.string().max(64).optional(), + toolbox2Enabled: z.boolean().optional(), }); // PUT /api/site-settings - 更新网站设置(仅管理员) @@ -122,6 +130,10 @@ export async function PUT(request: NextRequest) { bannerEnabled: data.bannerEnabled ?? true, bannerText: data.bannerText ?? "升级会员, Nano Banana Pro 免费365天!", bannerLink: data.bannerLink, + toolbox1Name: data.toolbox1Name ?? "工具箱", + toolbox1Enabled: data.toolbox1Enabled ?? true, + toolbox2Name: data.toolbox2Name ?? "工具箱2", + toolbox2Enabled: data.toolbox2Enabled ?? false, updatedBy: session.user.id, }) .returning(); diff --git a/app/canvas/[id]/layout.tsx b/app/canvas/[id]/layout.tsx index a4db2ff..cdb8e28 100644 --- a/app/canvas/[id]/layout.tsx +++ b/app/canvas/[id]/layout.tsx @@ -40,8 +40,13 @@ async function CanvasLayoutWrapper({ children }: { children: React.ReactNode }) id: link.id, name: link.name, url: link.url, - order: link.order + order: link.order, + toolboxGroup: link.toolboxGroup || 1, }))} + toolbox1Name={siteSettings.toolbox1Name || "工具箱"} + toolbox1Enabled={siteSettings.toolbox1Enabled ?? true} + toolbox2Name={siteSettings.toolbox2Name || "工具箱2"} + toolbox2Enabled={siteSettings.toolbox2Enabled ?? false} />
{children} diff --git a/components/top-navbar.tsx b/components/top-navbar.tsx index cb54f7c..9943aa9 100644 --- a/components/top-navbar.tsx +++ b/components/top-navbar.tsx @@ -22,17 +22,24 @@ type RankingItem = { featuredCount: number; }; +type CustomLink = { + id: string; + name: string; + url: string; + order: number; + toolboxGroup?: number; // 1 或 2 +}; + type TopNavbarProps = { siteName?: string; siteLogo?: string; siteLogoDark?: string; isAdmin?: boolean; - customLinks?: Array<{ - id: string; - name: string; - url: string; - order: number; - }>; + customLinks?: CustomLink[]; + toolbox1Name?: string; + toolbox1Enabled?: boolean; + toolbox2Name?: string; + toolbox2Enabled?: boolean; }; export function TopNavbar({ @@ -41,6 +48,10 @@ export function TopNavbar({ siteLogoDark, isAdmin = false, customLinks = [], + toolbox1Name = "工具箱", + toolbox1Enabled = true, + toolbox2Name = "工具箱2", + toolbox2Enabled = false, }: TopNavbarProps) { const router = useRouter(); const { resolvedTheme } = useTheme(); @@ -145,8 +156,8 @@ export function TopNavbar({ {/* 右侧区域:自定义链接 + 系统设置 */}
- {/* 工具箱 (如果有自定义链接) */} - {customLinks.length > 0 && ( + {/* 工具箱1 (如果启用且有链接) */} + {toolbox1Enabled && customLinks.filter(l => (l.toolboxGroup || 1) === 1).length > 0 && (
{customLinks + .filter(l => (l.toolboxGroup || 1) === 1) + .sort((a, b) => a.order - b.order) + .map((link) => ( + + + + {link.name} + + + ))} +
+
+
+ )} + + {/* 工具箱2 (如果启用且有链接) */} + {toolbox2Enabled && customLinks.filter(l => l.toolboxGroup === 2).length > 0 && ( + + + + + +
+ {customLinks + .filter(l => l.toolboxGroup === 2) .sort((a, b) => a.order - b.order) .map((link) => ( diff --git a/lib/db/queries.ts b/lib/db/queries.ts index c233755..2c5a273 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -1508,29 +1508,25 @@ export async function getSubModelsByParentModelId(parentModelId: string) { } export async function getSiteSettings() { + const defaultSettings = { + siteName: "AI Chatbot", + siteLogo: null, + siteLogoDark: null, + favicon: null, + defaultTheme: "system", + bannerEnabled: true, + bannerText: "升级会员, Nano Banana Pro 免费365天!", + bannerLink: null, + toolbox1Name: "工具箱", + toolbox1Enabled: true, + toolbox2Name: "工具箱2", + toolbox2Enabled: false, + }; try { const [settings] = await db.select().from(siteSettings).limit(1); - return settings || { - siteName: "AI Chatbot", - siteLogo: null, - siteLogoDark: null, - favicon: null, - defaultTheme: "system", - bannerEnabled: true, - bannerText: "升级会员, Nano Banana Pro 免费365天!", - bannerLink: null - }; + return settings || defaultSettings; } catch (_error) { - return { - siteName: "AI Chatbot", - siteLogo: null, - siteLogoDark: null, - favicon: null, - defaultTheme: "system", - bannerEnabled: true, - bannerText: "升级会员, Nano Banana Pro 免费365天!", - bannerLink: null - }; + return defaultSettings; } } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 506f132..505711b 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -296,6 +296,11 @@ export const siteSettings = pgTable("SiteSettings", { bannerEnabled: boolean("bannerEnabled").notNull().default(true), // 是否显示广告横幅 bannerText: text("bannerText").default("升级会员, Nano Banana Pro 免费365天!"), // 广告横幅文字 bannerLink: text("bannerLink"), // 广告横幅链接(可选) + // 工具箱设置 + toolbox1Name: varchar("toolbox1Name", { length: 64 }).default("工具箱"), // 工具箱1名称 + toolbox1Enabled: boolean("toolbox1Enabled").notNull().default(true), // 是否显示工具箱1 + toolbox2Name: varchar("toolbox2Name", { length: 64 }).default("工具箱2"), // 工具箱2名称 + toolbox2Enabled: boolean("toolbox2Enabled").notNull().default(false), // 是否显示工具箱2 updatedAt: timestamp("updatedAt").notNull().defaultNow(), updatedBy: uuid("updatedBy").references(() => user.id), }); @@ -309,6 +314,7 @@ export const navLinks = pgTable("NavLinks", { url: text("url").notNull(), order: integer("order").notNull().default(0), isEnabled: boolean("isEnabled").notNull().default(true), + toolboxGroup: integer("toolboxGroup").notNull().default(1), // 属于哪个工具箱:1 或 2 createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), createdBy: uuid("createdBy").references(() => user.id),