mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 01:14:36 +08:00
feat(docs): add documentation link in top menu and user status actions
This commit is contained in:
@@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
|
|||||||
|
|
||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
|
- 画布左上角菜单和右上角状态栏新增“文档”入口,会使用 `NEXT_PUBLIC_DOC_URL` 配置的地址并在新标签打开文档站;需要验证登录和未登录状态下顶部入口都可见。
|
||||||
- 文档站搜索改为中英文混合 tokenizer,中文正文、标题和短语会按中文词、单字、二元和三元片段建立索引;需要验证 `/api/search` 和搜索弹窗能命中文档中的中文关键词。
|
- 文档站搜索改为中英文混合 tokenizer,中文正文、标题和短语会按中文词、单字、二元和三元片段建立索引;需要验证 `/api/search` 和搜索弹窗能命中文档中的中文关键词。
|
||||||
- 文档站改为 Next.js standalone server 输出,新增 `docs/Dockerfile`、`docs/docker-compose.yml` 和 `docs/docker-compose.local.yml` 独立运行入口;需要验证文档页、搜索接口和 LLM 文本接口在文档站容器中可访问。
|
- 文档站改为 Next.js standalone server 输出,新增 `docs/Dockerfile`、`docs/docker-compose.yml` 和 `docs/docker-compose.local.yml` 独立运行入口;需要验证文档页、搜索接口和 LLM 文本接口在文档站容器中可访问。
|
||||||
- 画布连线支持右键打开删除菜单;拖拽连线到目标卡片内部、连接点附近或卡片边缘外扩范围内会自动吸附并连接,拖到已有但不可连接的节点附近不会再误弹创建节点菜单。
|
- 画布连线支持右键打开删除菜单;拖拽连线到目标卡片内部、连接点附近或卡片边缘外扩范围内会自动吸附并连接,拖到已有但不可连接的节点附近不会再误弹创建节点菜单。
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
|
import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { Home, ImageIcon, Images, List, Menu, MessageSquare, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react";
|
import { BookOpen, Home, ImageIcon, Images, List, Menu, MessageSquare, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
import { requestEdit, requestGeneration, requestImageQuestion } from "@/services/api/image";
|
import { requestEdit, requestGeneration, requestImageQuestion } from "@/services/api/image";
|
||||||
import { requestVideoGeneration, storeGeneratedVideo } from "@/services/api/video";
|
import { requestVideoGeneration, storeGeneratedVideo } from "@/services/api/video";
|
||||||
|
import { DOCS_URL } from "@/constant/env";
|
||||||
import { defaultConfig, type AiConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
import { defaultConfig, type AiConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||||
import { resolveImageUrl, uploadImage, type UploadedImage } from "@/services/image-storage";
|
import { resolveImageUrl, uploadImage, type UploadedImage } from "@/services/image-storage";
|
||||||
import { resolveMediaUrl, uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
import { resolveMediaUrl, uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
||||||
@@ -2441,6 +2442,7 @@ function CanvasTopBar({
|
|||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
{ key: "home", icon: <Home className="size-4" />, label: "主页", onClick: onHome },
|
{ key: "home", icon: <Home className="size-4" />, label: "主页", onClick: onHome },
|
||||||
|
{ key: "docs", icon: <BookOpen className="size-4" />, label: "文档", onClick: () => window.open(DOCS_URL, "_blank", "noopener,noreferrer") },
|
||||||
{ key: "projects", icon: <Images className="size-4" />, label: "我的画布", onClick: onProjects },
|
{ key: "projects", icon: <Images className="size-4" />, label: "我的画布", onClick: onProjects },
|
||||||
{ type: "divider" },
|
{ type: "divider" },
|
||||||
{ key: "new", icon: <Plus className="size-4" />, label: "新建画布", onClick: onCreateProject },
|
{ key: "new", icon: <Plus className="size-4" />, label: "新建画布", onClick: onCreateProject },
|
||||||
|
|||||||
@@ -1,30 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Menu, Settings2 } from "lucide-react";
|
import { Menu } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
|
|
||||||
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
||||||
import { AppConfigModal } from "@/components/layout/app-config-modal";
|
import { AppConfigModal } from "@/components/layout/app-config-modal";
|
||||||
import { GitHubLink } from "@/components/layout/github-link";
|
|
||||||
import { MobileNavDrawer } from "@/components/layout/mobile-nav-drawer";
|
import { MobileNavDrawer } from "@/components/layout/mobile-nav-drawer";
|
||||||
import { UserStatusActions } from "@/components/layout/user-status-actions";
|
import { UserStatusActions } from "@/components/layout/user-status-actions";
|
||||||
import { VersionReleaseModal } from "@/components/layout/version-release-modal";
|
|
||||||
import { useConfigStore } from "@/stores/use-config-store";
|
|
||||||
import { useThemeStore } from "@/stores/use-theme-store";
|
|
||||||
import { useUserStore } from "@/stores/use-user-store";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export function AppTopNav() {
|
export function AppTopNav() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
|
||||||
const theme = useThemeStore((state) => state.theme);
|
|
||||||
const setTheme = useThemeStore((state) => state.setTheme);
|
|
||||||
const user = useUserStore((state) => state.user);
|
|
||||||
const isReady = useUserStore((state) => state.isReady);
|
|
||||||
const hideHeader = /^\/canvas\/[^/]+/.test(pathname);
|
const hideHeader = /^\/canvas\/[^/]+/.test(pathname);
|
||||||
const slug = pathname.split("/").filter(Boolean)[0];
|
const slug = pathname.split("/").filter(Boolean)[0];
|
||||||
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;
|
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;
|
||||||
@@ -80,33 +69,7 @@ export function AppTopNav() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-auto flex h-9 min-w-0 items-center justify-end gap-2 justify-self-end whitespace-nowrap">
|
<div className="my-auto flex h-9 min-w-0 items-center justify-end gap-2 justify-self-end whitespace-nowrap">
|
||||||
{isReady && user ? (
|
<UserStatusActions />
|
||||||
<UserStatusActions />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4"
|
|
||||||
onClick={() => openConfigDialog(false)}
|
|
||||||
aria-label="配置"
|
|
||||||
title="配置"
|
|
||||||
>
|
|
||||||
<Settings2 className="size-4" />
|
|
||||||
</button>
|
|
||||||
<AnimatedThemeToggler
|
|
||||||
theme={theme}
|
|
||||||
onThemeChange={setTheme}
|
|
||||||
className="inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4"
|
|
||||||
aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
|
||||||
title={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
|
||||||
/>
|
|
||||||
<VersionReleaseModal />
|
|
||||||
<GitHubLink />
|
|
||||||
<Link href="/login" className="text-sm font-medium text-stone-600 underline-offset-4 transition hover:text-stone-950 hover:underline dark:text-stone-300 dark:hover:text-stone-100">
|
|
||||||
登录
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { CSSProperties, RefObject } from "react";
|
import type { CSSProperties, RefObject } from "react";
|
||||||
import { Avatar, Dropdown, Tooltip } from "antd";
|
import { Avatar, Dropdown, Tooltip } from "antd";
|
||||||
import { Keyboard, LogOut, Settings2, Shield } from "lucide-react";
|
import { BookOpen, Keyboard, LogOut, Settings2, Shield } from "lucide-react";
|
||||||
import type { ItemType } from "antd/es/menu/interface";
|
import type { ItemType } from "antd/es/menu/interface";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
|
|||||||
import { GitHubLink } from "@/components/layout/github-link";
|
import { GitHubLink } from "@/components/layout/github-link";
|
||||||
import { VersionReleaseModal } from "@/components/layout/version-release-modal";
|
import { VersionReleaseModal } from "@/components/layout/version-release-modal";
|
||||||
import { CreditSymbol } from "@/constant/credits";
|
import { CreditSymbol } from "@/constant/credits";
|
||||||
|
import { DOCS_URL } from "@/constant/env";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { canvasThemes } from "@/lib/canvas-theme";
|
import { canvasThemes } from "@/lib/canvas-theme";
|
||||||
import { useConfigStore } from "@/stores/use-config-store";
|
import { useConfigStore } from "@/stores/use-config-store";
|
||||||
@@ -37,10 +38,10 @@ export function UserStatusActions({ showConfig = true, variant = "default", onOp
|
|||||||
const credits = user?.credits ?? 0;
|
const credits = user?.credits ?? 0;
|
||||||
const avatarUrl = user?.avatarUrl?.trim();
|
const avatarUrl = user?.avatarUrl?.trim();
|
||||||
const avatarText = (userName.trim()[0] || "U").toUpperCase();
|
const avatarText = (userName.trim()[0] || "U").toUpperCase();
|
||||||
const naturalIconClass = "inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4";
|
const naturalIconClass = "inline-flex size-7 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4";
|
||||||
const iconStyle: CSSProperties | undefined = variant === "canvas" ? { color: canvasTheme.node.text } : undefined;
|
const iconStyle: CSSProperties | undefined = variant === "canvas" ? { color: canvasTheme.node.text } : undefined;
|
||||||
const versionStyle = iconStyle;
|
const versionStyle = iconStyle;
|
||||||
const gitHubClassName = variant === "canvas" ? "size-11 text-base" : undefined;
|
const gitHubClassName = "size-7 text-base";
|
||||||
const gitHubStyle = iconStyle;
|
const gitHubStyle = iconStyle;
|
||||||
const avatarStyle: CSSProperties | undefined = variant === "canvas" ? { borderColor: canvasTheme.toolbar.border, color: canvasTheme.node.text, background: "transparent" } : undefined;
|
const avatarStyle: CSSProperties | undefined = variant === "canvas" ? { borderColor: canvasTheme.toolbar.border, color: canvasTheme.node.text, background: "transparent" } : undefined;
|
||||||
const menuItems: ItemType[] = [
|
const menuItems: ItemType[] = [
|
||||||
@@ -52,7 +53,10 @@ export function UserStatusActions({ showConfig = true, variant = "default", onOp
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex shrink-0 items-center gap-1.5">
|
<div className="inline-flex shrink-0 items-center gap-1">
|
||||||
|
<a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className={naturalIconClass} style={iconStyle} aria-label="文档" title="文档">
|
||||||
|
<BookOpen className="size-4" />
|
||||||
|
</a>
|
||||||
{showConfig ? (
|
{showConfig ? (
|
||||||
<button type="button" className={naturalIconClass} style={iconStyle} onClick={() => openConfigDialog(false)} aria-label="配置" title="配置">
|
<button type="button" className={naturalIconClass} style={iconStyle} onClick={() => openConfigDialog(false)} aria-label="配置" title="配置">
|
||||||
<Settings2 className="size-4" />
|
<Settings2 className="size-4" />
|
||||||
@@ -82,12 +86,12 @@ export function UserStatusActions({ showConfig = true, variant = "default", onOp
|
|||||||
{user ? (
|
{user ? (
|
||||||
<div ref={accountRef}>
|
<div ref={accountRef}>
|
||||||
<Dropdown open={accountOpen} onOpenChange={onAccountOpenChange} trigger={["click"]} placement="bottomRight" getPopupContainer={getPopupContainer} styles={{ root: { minWidth: 150 } }} menu={{ items: menuItems }}>
|
<Dropdown open={accountOpen} onOpenChange={onAccountOpenChange} trigger={["click"]} placement="bottomRight" getPopupContainer={getPopupContainer} styles={{ root: { minWidth: 150 } }} menu={{ items: menuItems }}>
|
||||||
<button type="button" className="flex size-8 shrink-0 items-center justify-center rounded-full bg-transparent p-0 text-[0] leading-[0] transition" aria-label="账户菜单">
|
<button type="button" className="flex size-7 shrink-0 items-center justify-center rounded-full bg-transparent p-0 text-[0] leading-[0] transition" aria-label="账户菜单">
|
||||||
<Avatar
|
<Avatar
|
||||||
size={28}
|
size={24}
|
||||||
src={avatarUrl ? <img src={avatarUrl} alt={userName} referrerPolicy="no-referrer" /> : undefined}
|
src={avatarUrl ? <img src={avatarUrl} alt={userName} referrerPolicy="no-referrer" /> : undefined}
|
||||||
alt={userName}
|
alt={userName}
|
||||||
className="!flex !items-center !justify-center border border-stone-300 bg-transparent text-xs font-semibold text-stone-800 transition hover:border-stone-500 hover:text-stone-950 dark:border-stone-700 dark:text-stone-100 dark:hover:border-stone-400 dark:hover:text-white"
|
className="!flex !items-center !justify-center border border-stone-300 bg-transparent text-[11px] font-semibold text-stone-800 transition hover:border-stone-500 hover:text-stone-950 dark:border-stone-700 dark:text-stone-100 dark:hover:border-stone-400 dark:hover:text-white"
|
||||||
style={avatarStyle}
|
style={avatarStyle}
|
||||||
>
|
>
|
||||||
{avatarText}
|
{avatarText}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||||
|
|
||||||
|
export const DOCS_URL = process.env.NEXT_PUBLIC_DOC_URL || "https://docs.canvas.best";
|
||||||
|
|||||||
Reference in New Issue
Block a user