diff --git a/frontend/app/(main)/dashboard/page.tsx b/frontend/app/(main)/dashboard/page.tsx index 6b6b8bff..64bd8aab 100644 --- a/frontend/app/(main)/dashboard/page.tsx +++ b/frontend/app/(main)/dashboard/page.tsx @@ -8,10 +8,8 @@ export const metadata: Metadata = { export default function DashboardPage() { return ( -
- - - -
+ + + ); } diff --git a/frontend/app/(main)/layout.tsx b/frontend/app/(main)/layout.tsx index 7a5039ae..b9c52135 100644 --- a/frontend/app/(main)/layout.tsx +++ b/frontend/app/(main)/layout.tsx @@ -15,8 +15,10 @@ export default function ProjectLayout({
-
- {children} +
+
+ {children} +
diff --git a/frontend/app/(main)/project/page.tsx b/frontend/app/(main)/project/page.tsx index eca50570..21b51320 100644 --- a/frontend/app/(main)/project/page.tsx +++ b/frontend/app/(main)/project/page.tsx @@ -8,10 +8,8 @@ export const metadata: Metadata = { export default function ProjectPage() { return ( -
- - - -
+ + + ); } diff --git a/frontend/app/(main)/receive/[projectId]/page.tsx b/frontend/app/(main)/receive/[projectId]/page.tsx index 0d629034..763bddad 100644 --- a/frontend/app/(main)/receive/[projectId]/page.tsx +++ b/frontend/app/(main)/receive/[projectId]/page.tsx @@ -3,10 +3,8 @@ import {ReceiveMain} from '@/components/common/receive'; export default function ProjectPage() { return ( -
- - - -
+ + + ); } diff --git a/frontend/app/(main)/received/page.tsx b/frontend/app/(main)/received/page.tsx index 37cf0996..23980a12 100644 --- a/frontend/app/(main)/received/page.tsx +++ b/frontend/app/(main)/received/page.tsx @@ -8,10 +8,8 @@ export const metadata: Metadata = { export default function ReceivedPage() { return ( -
- - - -
+ + + ); } diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx index 0251dfb3..ce6a20ba 100644 --- a/frontend/app/not-found.tsx +++ b/frontend/app/not-found.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; export default function NotFound() { return (
-
+
); } - diff --git a/frontend/components/animate-ui/radix/dialog.tsx b/frontend/components/animate-ui/radix/dialog.tsx index 6752ce6f..1a638cab 100644 --- a/frontend/components/animate-ui/radix/dialog.tsx +++ b/frontend/components/animate-ui/radix/dialog.tsx @@ -11,6 +11,7 @@ import { } from 'motion/react'; import {cn} from '@/lib/utils'; +import {ScrollArea} from '@/components/ui/scroll-area'; type DialogContextType = { isOpen: boolean; @@ -151,7 +152,7 @@ function DialogContent({ }} transition={{...transition, duration: 0.15, ease: 'easeOut'}} className={cn( - 'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border border-border/60 bg-background/95 p-6 shadow-[0_24px_60px_rgba(15,23,42,0.12)] ring-1 ring-black/[0.03] dark:border-border/70 dark:bg-background dark:shadow-[0_24px_60px_rgba(0,0,0,0.45)] dark:ring-white/[0.04]', + 'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-0 overflow-hidden rounded-[24px] border border-border/50 bg-background/95 shadow-[0_24px_60px_rgba(15,23,42,0.10)] ring-1 ring-black/[0.03] dark:border-border/70 dark:bg-background dark:shadow-[0_24px_60px_rgba(0,0,0,0.42)] dark:ring-white/[0.04]', className, )} {...props} @@ -178,7 +179,7 @@ function DialogHeader({className, ...props}: DialogHeaderProps) {
; + +function DialogBody({className, children, ...props}: DialogBodyProps) { + return ( + + {children} + + ); +} + type DialogTitleProps = React.ComponentProps; function DialogTitle({className, ...props}: DialogTitleProps) { @@ -224,7 +239,7 @@ function DialogDescription({className, ...props}: DialogDescriptionProps) { return ( ); @@ -239,6 +254,7 @@ export { DialogContent, DialogHeader, DialogFooter, + DialogBody, DialogTitle, DialogDescription, useDialog, @@ -251,6 +267,7 @@ export { type DialogContentProps, type DialogHeaderProps, type DialogFooterProps, + type DialogBodyProps, type DialogTitleProps, type DialogDescriptionProps, }; diff --git a/frontend/components/common/auth/LoginForm.tsx b/frontend/components/common/auth/LoginForm.tsx index d4fa58ec..72dbb5d0 100644 --- a/frontend/components/common/auth/LoginForm.tsx +++ b/frontend/components/common/auth/LoginForm.tsx @@ -4,7 +4,7 @@ import {useState, useEffect} from 'react'; import {useSearchParams, useRouter} from 'next/navigation'; import {LiquidButton} from '@/components/animate-ui/buttons/liquid'; import {Accordion, AccordionItem, AccordionTrigger, AccordionContent} from '@/components/animate-ui/radix/accordion'; -import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription} from '@/components/animate-ui/radix/dialog'; +import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody} from '@/components/animate-ui/radix/dialog'; import {SquareArrowUpRight, LoaderCircle} from 'lucide-react'; import {LinuxDo} from '@/components/icons/logo'; import {useAuth} from '@/hooks/use-auth'; @@ -135,110 +135,112 @@ export function LoginForm({ 服务条款 - + 服务条款 - 请仔细阅读以下服务条款,使用本服务即表示您同意这些条款。 + 请仔细阅读以下服务条款,使用本服务即表示您同意这些条款。 -
- - - 1. 一般条款 - -
-

本服务条款(以下简称“条款”)规定了您使用 LINUX DO CDK 服务的条件。

-

通过访问或使用我们的服务,您同意受这些条款的约束。如果您不同意这些条款,请不要使用我们的服务。

-

我们保留随时修改这些条款的权利。修改后的条款将在发布后立即生效。

-
-
-
+ +
+ + + 1. 一般条款 + +
+

本服务条款(以下简称“条款”)规定了您使用 LINUX DO CDK 服务的条件。

+

通过访问或使用我们的服务,您同意受这些条款的约束。如果您不同意这些条款,请不要使用我们的服务。

+

我们保留随时修改这些条款的权利。修改后的条款将在发布后立即生效。

+
+
+
- - 2. 使用规则 - -
-

您同意以合法和负责任的方式使用我们的服务,严格遵守中华人民共和国相关法律法规。

-

禁止的行为包括但不限于:

-
    -
  • 发布非法、有害、威胁、辱骂、骚扰、诽谤或其他令人反感的内容
  • -
  • 尝试未经授权访问我们的系统或其他用户的账户
  • -
  • 干扰或破坏服务的正常运行
  • -
  • 违反任何适用的法律法规
  • -
-
-
-
+ + 2. 使用规则 + +
+

您同意以合法和负责任的方式使用我们的服务,严格遵守中华人民共和国相关法律法规。

+

禁止的行为包括但不限于:

+
    +
  • 发布非法、有害、威胁、辱骂、骚扰、诽谤或其他令人反感的内容
  • +
  • 尝试未经授权访问我们的系统或其他用户的账户
  • +
  • 干扰或破坏服务的正常运行
  • +
  • 违反任何适用的法律法规
  • +
+
+
+
- - 3. 内容规范 - -
-

为维护健康的网络环境,严格禁止分发以下类型的内容:

-
    -
  • 色情内容:任何包含色情、淫秽、暴露或性暗示的内容
  • -
  • 推广内容:商业广告、营销推广、垃圾信息或其他商业性质的内容
  • -
  • 违法内容:违反中华人民共和国法律法规的任何内容
  • -
  • 有害信息:暴力、恐怖主义、极端主义或危害国家安全的内容
  • -
  • 虚假信息:谣言、虚假新闻或误导性信息
  • -
  • 侵权内容:侵犯他人知识产权、隐私权或其他合法权益的内容
  • -
-

一旦发现违规内容,我们将立即删除并可能终止相关账户。

-
-
-
+ + 3. 内容规范 + +
+

为维护健康的网络环境,严格禁止分发以下类型的内容:

+
    +
  • 色情内容:任何包含色情、淫秽、暴露或性暗示的内容
  • +
  • 推广内容:商业广告、营销推广、垃圾信息或其他商业性质的内容
  • +
  • 违法内容:违反中华人民共和国法律法规的任何内容
  • +
  • 有害信息:暴力、恐怖主义、极端主义或危害国家安全的内容
  • +
  • 虚假信息:谣言、虚假新闻或误导性信息
  • +
  • 侵权内容:侵犯他人知识产权、隐私权或其他合法权益的内容
  • +
+

一旦发现违规内容,我们将立即删除并可能终止相关账户。

+
+
+
- - 4. 法律合规 - -
-

本服务严格遵守中华人民共和国相关法律法规,包括但不限于:

-
    -
  • 《中华人民共和国网络安全法》
  • -
  • 《中华人民共和国数据安全法》
  • -
  • 《中华人民共和国个人信息保护法》
  • -
  • 《互联网信息服务管理办法》
  • -
  • 《网络信息内容生态治理规定》
  • -
-

用户在使用服务时,必须遵守上述法律法规及其他相关规定。

-

对于违反法律法规的行为,我们将配合相关部门进行调查和处理。

-
-
-
+ + 4. 法律合规 + +
+

本服务严格遵守中华人民共和国相关法律法规,包括但不限于:

+
    +
  • 《中华人民共和国网络安全法》
  • +
  • 《中华人民共和国数据安全法》
  • +
  • 《中华人民共和国个人信息保护法》
  • +
  • 《互联网信息服务管理办法》
  • +
  • 《网络信息内容生态治理规定》
  • +
+

用户在使用服务时,必须遵守上述法律法规及其他相关规定。

+

对于违反法律法规的行为,我们将配合相关部门进行调查和处理。

+
+
+
- - 5. 账户责任 - -
-

您负责维护账户信息的准确性和安全性。

-

您对使用您账户进行的所有活动承担责任。

-

如果您发现任何未经授权的使用,请立即通知我们。

-
-
-
+ + 5. 账户责任 + +
+

您负责维护账户信息的准确性和安全性。

+

您对使用您账户进行的所有活动承担责任。

+

如果您发现任何未经授权的使用,请立即通知我们。

+
+
+
- - 6. 知识产权 - -
-

服务中的所有内容,包括文本、图形、徽标、图像和软件,均受版权和其他知识产权法保护。

-

未经我们明确书面许可,您不得复制、修改、分发或以其他方式使用这些内容。

-
-
-
+ + 6. 知识产权 + +
+

服务中的所有内容,包括文本、图形、徽标、图像和软件,均受版权和其他知识产权法保护。

+

未经我们明确书面许可,您不得复制、修改、分发或以其他方式使用这些内容。

+
+
+
- - 7. 责任限制 - -
-

在法律允许的最大范围内,我们不对任何间接、偶然、特殊或后果性损害承担责任。

-

我们的总责任不超过您在过去12个月内为服务支付的金额。

-
-
-
-
-
+ + 7. 责任限制 + +
+

在法律允许的最大范围内,我们不对任何间接、偶然、特殊或后果性损害承担责任。

+

我们的总责任不超过您在过去12个月内为服务支付的金额。

+
+
+
+
+
+
{' '}和{' '} @@ -248,109 +250,111 @@ export function LoginForm({ 隐私政策 - + 隐私政策 - 我们重视您的隐私,本政策说明我们如何收集、使用和保护您的个人信息。 + 我们重视您的隐私,本政策说明我们如何收集、使用和保护您的个人信息。 -
- - - 1. 信息收集 - -
-

我们收集以下类型的信息:

-
    -
  • 账户信息:通过 Linux Do OAuth 获取的用户名、头像等公开信息
  • -
  • 使用数据:您如何使用我们服务的信息
  • -
  • 技术信息:IP地址、设备信息、浏览器类型等
  • -
  • 日志信息:服务器日志和错误报告
  • -
-
-
-
+ +
+ + + 1. 信息收集 + +
+

我们收集以下类型的信息:

+
    +
  • 账户信息:通过 Linux Do OAuth 获取的用户名、头像等公开信息
  • +
  • 使用数据:您如何使用我们服务的信息
  • +
  • 技术信息:IP地址、设备信息、浏览器类型等
  • +
  • 日志信息:服务器日志和错误报告
  • +
+
+
+
- - 2. 信息使用 - -
-

我们使用收集的信息用于:

-
    -
  • 提供和维护我们的服务
  • -
  • 改善用户体验
  • -
  • 防止欺诈和滥用
  • -
  • 遵守法律义务
  • -
  • 发送重要通知
  • -
-
-
-
+ + 2. 信息使用 + +
+

我们使用收集的信息用于:

+
    +
  • 提供和维护我们的服务
  • +
  • 改善用户体验
  • +
  • 防止欺诈和滥用
  • +
  • 遵守法律义务
  • +
  • 发送重要通知
  • +
+
+
+
- - 3. 信息共享 - -
-

我们不会出售、交换或出租您的个人信息给第三方。

-

在以下情况下,我们可能会共享您的信息:

-
    -
  • 经您明确同意
  • -
  • 法律要求或政府请求
  • -
  • 保护我们的权利和财产
  • -
  • 与可信的服务提供商合作(受保密协议约束)
  • -
-
-
-
+ + 3. 信息共享 + +
+

我们不会出售、交换或出租您的个人信息给第三方。

+

在以下情况下,我们可能会共享您的信息:

+
    +
  • 经您明确同意
  • +
  • 法律要求或政府请求
  • +
  • 保护我们的权利和财产
  • +
  • 与可信的服务提供商合作(受保密协议约束)
  • +
+
+
+
- - 4. 数据安全 - -
-

我们采用业界标准的安全措施保护您的信息:

-
    -
  • 数据传输加密(HTTPS/TLS)
  • -
  • 数据库访问控制和加密
  • -
  • 定期安全审计和漏洞扫描
  • -
  • 员工访问权限管理
  • -
-

但请注意,没有任何数据传输或存储方法是100%安全的。

-
-
-
+ + 4. 数据安全 + +
+

我们采用业界标准的安全措施保护您的信息:

+
    +
  • 数据传输加密(HTTPS/TLS)
  • +
  • 数据库访问控制和加密
  • +
  • 定期安全审计和漏洞扫描
  • +
  • 员工访问权限管理
  • +
+

但请注意,没有任何数据传输或存储方法是100%安全的。

+
+
+
- - 5. 数据保留 - -
-

我们仅在必要的时间内保留您的个人信息:

-
    -
  • 账户信息:账户存在期间
  • -
  • 使用日志:90天
  • -
  • 安全日志:1年
  • -
-

您可以随时要求删除您的账户和相关数据。

-
-
-
+ + 5. 数据保留 + +
+

我们仅在必要的时间内保留您的个人信息:

+
    +
  • 账户信息:账户存在期间
  • +
  • 使用日志:90天
  • +
  • 安全日志:1年
  • +
+

您可以随时要求删除您的账户和相关数据。

+
+
+
- - 6. 您的权利 - -
-

您对您的个人信息享有以下权利:

-
    -
  • 访问权:查看我们持有的关于您的信息
  • -
  • 更正权:更正不准确的信息
  • -
  • 删除权:要求删除您的个人信息
  • -
  • 限制处理权:限制我们处理您信息的方式
  • -
-
-
-
-
-
+ + 6. 您的权利 + +
+

您对您的个人信息享有以下权利:

+
    +
  • 访问权:查看我们持有的关于您的信息
  • +
  • 更正权:更正不准确的信息
  • +
  • 删除权:要求删除您的个人信息
  • +
  • 限制处理权:限制我们处理您信息的方式
  • +
+
+
+
+
+
+
diff --git a/frontend/components/common/dashboard/DashboardMain.tsx b/frontend/components/common/dashboard/DashboardMain.tsx index ee8e4b46..ca7112b4 100644 --- a/frontend/components/common/dashboard/DashboardMain.tsx +++ b/frontend/components/common/dashboard/DashboardMain.tsx @@ -147,15 +147,15 @@ export function DashboardMain() { variants={containerVariants} > {/* 问候语标题和时间选择器 */} - -
-

+ +
+

{getTimeGreeting()}好,{user?.username || 'Linux Do User'}

{/* 时间范围选择器 */} -
+
setRange(Number(value))} diff --git a/frontend/components/common/layout/ManagementBar.tsx b/frontend/components/common/layout/ManagementBar.tsx index aa08dcb7..4360bdea 100644 --- a/frontend/components/common/layout/ManagementBar.tsx +++ b/frontend/components/common/layout/ManagementBar.tsx @@ -1,5 +1,6 @@ -import {useState, useEffect} from 'react'; +import {useState, useEffect, useRef, useCallback} from 'react'; import {FloatingDock} from '@/components/ui/floating-dock'; +import packageJson from '../../../package.json'; import { MessageCircleIcon, SendIcon, @@ -13,6 +14,7 @@ import { LinkIcon, FolderGit2Icon, Book, + ChevronRight, } from 'lucide-react'; import {useThemeUtils} from '@/hooks/use-theme-utils'; import {useAuth} from '@/hooks/use-auth'; @@ -22,10 +24,10 @@ import {Button} from '@/components/ui/button'; import Link from 'next/link'; import {PaymentSettingsDialog} from '@/components/common/payment'; import {Badge} from '@/components/ui/badge'; -import {ScrollArea} from '@/components/ui/scroll-area'; import {Separator} from '@/components/ui/separator'; import { Dialog, + DialogBody, DialogContent, DialogHeader, DialogDescription, @@ -39,6 +41,22 @@ const IconOptions = { className: 'h-4 w-4', } as const; +const DOCK_STORAGE_KEY = 'linux-do-cdk:dock-position'; +const DOCK_TIP_STORAGE_KEY = 'linux-do-cdk:dock-tip-dismissed'; +const DOCK_MARGIN = 16; +const DOCK_LONG_PRESS_MS = 180; +const DOCK_CLICK_SUPPRESS_MS = 220; +const DOCK_INTERACTIVE_SELECTOR = 'a,button,input,textarea,select,[role="button"],[data-dock-no-drag="true"]'; + +type DockViewport = 'desktop' | 'mobile'; + +type DockPosition = { + x: number; + y: number; +}; + +type DockPositions = Partial>; + /** * 获取信任等级对应的文本描述 */ @@ -73,13 +91,133 @@ export function ManagementBar() { const themeUtils = useThemeUtils(); const {user, isLoading, logout} = useAuth(); const [mounted, setMounted] = useState(false); + const [dockViewport, setDockViewport] = useState('desktop'); const [profileOpen, setProfileOpen] = useState(false); const [paymentOpen, setPaymentOpen] = useState(false); + const [dockPosition, setDockPosition] = useState(null); + const [showDockTip, setShowDockTip] = useState(false); + const [dockTipStep, setDockTipStep] = useState(0); + const dockRef = useRef(null); + const dockViewportRef = useRef('desktop'); + const dragOffsetRef = useRef({x: 0, y: 0}); + const dockPressTimerRef = useRef(null); + const pressStartRef = useRef({x: 0, y: 0}); + const isDraggingRef = useRef(false); + const suppressClickUntilRef = useRef(0); + + const getViewport = useCallback((): DockViewport => (window.innerWidth >= 768 ? 'desktop' : 'mobile'), []); + + const readDockPositions = useCallback((): DockPositions => { + if (typeof window === 'undefined') return {}; + + try { + const raw = window.localStorage.getItem(DOCK_STORAGE_KEY); + return raw ? JSON.parse(raw) as DockPositions : {}; + } catch { + return {}; + } + }, []); + + const writeDockPosition = useCallback((viewport: DockViewport, position: DockPosition) => { + if (typeof window === 'undefined') return; + + const nextPositions = { + ...readDockPositions(), + [viewport]: position, + }; + + window.localStorage.setItem(DOCK_STORAGE_KEY, JSON.stringify(nextPositions)); + }, [readDockPositions]); + + const getDockRect = useCallback(() => { + const rect = dockRef.current?.getBoundingClientRect(); + return { + width: rect?.width ?? (dockViewportRef.current === 'desktop' ? 420 : 52), + height: rect?.height ?? (dockViewportRef.current === 'desktop' ? 88 : 52), + }; + }, []); + + const clampDockPosition = useCallback((position: DockPosition): DockPosition => { + if (typeof window === 'undefined') return position; + + const {width, height} = getDockRect(); + const maxX = Math.max(DOCK_MARGIN, window.innerWidth - width - DOCK_MARGIN); + const maxY = Math.max(DOCK_MARGIN, window.innerHeight - height - DOCK_MARGIN); + + return { + x: Math.min(Math.max(position.x, DOCK_MARGIN), maxX), + y: Math.min(Math.max(position.y, DOCK_MARGIN), maxY), + }; + }, [getDockRect]); + + const getDefaultDockPosition = useCallback((viewport: DockViewport): DockPosition => { + if (typeof window === 'undefined') return {x: DOCK_MARGIN, y: DOCK_MARGIN}; + + const {width, height} = getDockRect(); + const basePosition = viewport === 'desktop' ? + { + x: (window.innerWidth - width) / 2, + y: window.innerHeight - height - DOCK_MARGIN, + } : + { + x: window.innerWidth - width - DOCK_MARGIN, + y: window.innerHeight - height - DOCK_MARGIN, + }; + + return clampDockPosition(basePosition); + }, [clampDockPosition, getDockRect]); + + const syncDockPosition = useCallback((nextViewport?: DockViewport) => { + if (typeof window === 'undefined') return; + + const viewport = nextViewport ?? getViewport(); + dockViewportRef.current = viewport; + setDockViewport(viewport); + + const savedPosition = readDockPositions()[viewport]; + const nextPosition = clampDockPosition(savedPosition ?? getDefaultDockPosition(viewport)); + setDockPosition(nextPosition); + }, [clampDockPosition, getDefaultDockPosition, getViewport, readDockPositions]); useEffect(() => { setMounted(true); }, []); + useEffect(() => { + if (!mounted || typeof window === 'undefined') return; + + const dismissed = window.localStorage.getItem(DOCK_TIP_STORAGE_KEY) === 'true'; + if (!dismissed) { + setShowDockTip(true); + } + }, [mounted]); + + useEffect(() => { + if (!mounted) return; + + const frameId = window.requestAnimationFrame(() => { + syncDockPosition(); + }); + + const handleResize = () => { + window.requestAnimationFrame(() => { + const viewport = getViewport(); + const savedPosition = readDockPositions()[viewport]; + + dockViewportRef.current = viewport; + setDockViewport(viewport); + setDockPosition((current) => clampDockPosition(savedPosition ?? current ?? getDefaultDockPosition(viewport))); + }); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.cancelAnimationFrame(frameId); + window.removeEventListener('resize', handleResize); + }; + }, [clampDockPosition, getDefaultDockPosition, getViewport, mounted, readDockPositions, syncDockPosition]); + useEffect(() => { const handleOpenPaymentSettings = () => { setProfileOpen(false); @@ -98,6 +236,109 @@ export function ManagementBar() { }); }; + const handleDismissDockTip = useCallback(() => { + setShowDockTip(false); + setDockTipStep(0); + if (typeof window !== 'undefined') { + window.localStorage.setItem(DOCK_TIP_STORAGE_KEY, 'true'); + } + }, []); + + const dockTipSteps = dockViewport === 'mobile' ? + [ + '菜单栏已调整至此处,点击即可展开。', + '长按菜单栏空白区域可自定义拖动位置。', + '展开后,倒数第二个按钮用于快速创建项目。', + '展开后,倒数第一个按钮用于访问基础设置与账户信息。', + ] : + [ + '菜单栏已调整至此处,长按空白区域可自定义拖动位置。', + '右侧第二个按钮用于快速创建项目。', + '右侧第一个按钮用于访问基础设置与账户信息。', + ]; + + const handleNextDockTip = useCallback(() => { + setDockTipStep((current) => { + if (current >= dockTipSteps.length - 1) { + handleDismissDockTip(); + return current; + } + return current + 1; + }); + }, [dockTipSteps.length, handleDismissDockTip]); + + const clearDockPressTimer = useCallback(() => { + if (dockPressTimerRef.current !== null) { + window.clearTimeout(dockPressTimerRef.current); + dockPressTimerRef.current = null; + } + }, []); + + const beginDockDrag = useCallback((clientX: number, clientY: number) => { + if (!dockPosition || typeof window === 'undefined') return; + + const viewport = getViewport(); + dockViewportRef.current = viewport; + dragOffsetRef.current = { + x: clientX - dockPosition.x, + y: clientY - dockPosition.y, + }; + isDraggingRef.current = true; + + const handlePointerMove = (moveEvent: PointerEvent) => { + const nextPosition = clampDockPosition({ + x: moveEvent.clientX - dragOffsetRef.current.x, + y: moveEvent.clientY - dragOffsetRef.current.y, + }); + + setDockPosition(nextPosition); + }; + + const handlePointerUp = (upEvent: PointerEvent) => { + const nextPosition = clampDockPosition({ + x: upEvent.clientX - dragOffsetRef.current.x, + y: upEvent.clientY - dragOffsetRef.current.y, + }); + + setDockPosition(nextPosition); + writeDockPosition(dockViewportRef.current, nextPosition); + isDraggingRef.current = false; + suppressClickUntilRef.current = Date.now() + DOCK_CLICK_SUPPRESS_MS; + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + window.removeEventListener('pointercancel', handlePointerUp); + }; + + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + window.addEventListener('pointercancel', handlePointerUp); + }, [clampDockPosition, dockPosition, getViewport, writeDockPosition]); + + const handleDockPointerDown = useCallback((event: React.PointerEvent) => { + if (!dockPosition || event.button !== 0) return; + if ((event.target as HTMLElement).closest(DOCK_INTERACTIVE_SELECTOR)) return; + + pressStartRef.current = {x: event.clientX, y: event.clientY}; + clearDockPressTimer(); + dockPressTimerRef.current = window.setTimeout(() => { + beginDockDrag(pressStartRef.current.x, pressStartRef.current.y); + dockPressTimerRef.current = null; + }, DOCK_LONG_PRESS_MS); + }, [beginDockDrag, clearDockPressTimer, dockPosition]); + + const handleDockPointerEnd = useCallback(() => { + if (!isDraggingRef.current) { + clearDockPressTimer(); + } + }, [clearDockPressTimer]); + + const handleDockClickCapture = useCallback((event: React.MouseEvent) => { + if (Date.now() > suppressClickUntilRef.current) return; + + event.preventDefault(); + event.stopPropagation(); + }, []); + const dockItems = [ { title: '实时数据', @@ -142,15 +383,15 @@ export function ManagementBar() { - - 个人信息 - + + 个人信息 + 管理账户信息、主题偏好和支付设置 - +
{!isLoading && user && ( <> @@ -330,7 +571,7 @@ export function ManagementBar() {
关于 LINUX DO CDK
-
Version 1.2.3, Build At 2026-04-22
+
Version {packageJson.version}, Build At 2026-04-22
LINUX DO CDK 是一个为 Linux Do 社区打造的内容分发工具平台,旨在提供快速、安全、便捷的 CDK 分享服务。平台支持多种分发方式,具备完善的用户权限管理和风险控制机制。
@@ -343,7 +584,7 @@ export function ManagementBar() {
)}
- + @@ -353,7 +594,52 @@ export function ManagementBar() { ]; return ( -
+
+ {showDockTip && ( +
event.stopPropagation()} + onPointerUp={(event) => event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > +
+
+ 菜单栏引导 +
+

+ {dockTipSteps[dockTipStep]} +

+
+
+ {dockTipSteps.map((_, index) => ( + + ))} +
+ +
+
+
+ )} -
- {config?.has_config && ( - - )} - -
+ + {config?.has_config && ( + + + + + + 移除商户配置 + + + 移除后将清空当前保存的 Client ID 与 Client Secret。后续如需使用付费项目功能,需要重新配置。 + + + + + 取消 + + + {deleting ? '移除中...' : '确认移除'} + + + + + )} + ); diff --git a/frontend/components/common/project/BulkImportSection.tsx b/frontend/components/common/project/BulkImportSection.tsx index 0b2ebd13..0330df77 100644 --- a/frontend/components/common/project/BulkImportSection.tsx +++ b/frontend/components/common/project/BulkImportSection.tsx @@ -97,7 +97,7 @@ export function BulkImportSection({
-
{contentLabel}
+
{contentLabel}
@@ -173,7 +173,7 @@ export function BulkImportSection({
- + {items.length > 0 && (
- +
-
+ ) : ( - - + - - +
-
-
+ - - +
)}
-
-
-
-
+ + + + )} - + {createSuccess ? ( @@ -476,7 +474,7 @@ export function CreateDialog({ onClick={handleSubmit} disabled={loading || formData.distributionType === DistributionType.INVITE} size="sm" - className="min-w-16" + className="rounded-full px-3 text-xs shadow-none" > {loading ? '创建中...' : formData.distributionType === DistributionType.INVITE ? '开发中' : diff --git a/frontend/components/common/project/DistributionModeSelect.tsx b/frontend/components/common/project/DistributionModeSelect.tsx index 4fc7ac57..05579476 100644 --- a/frontend/components/common/project/DistributionModeSelect.tsx +++ b/frontend/components/common/project/DistributionModeSelect.tsx @@ -37,7 +37,7 @@ export function DistributionModeSelect({ }: DistributionModeSelectProps) { return (
- +
{DISTRIBUTION_OPTIONS.map((option) => { const Icon = option.icon; @@ -51,7 +51,7 @@ export function DistributionModeSelect({ aria-pressed={isActive} onClick={() => onDistributionTypeChange(option.type)} className={cn( - 'flex w-full items-start gap-3 rounded-2xl border-none px-3.5 py-3 text-left shadow-none', + 'flex w-full items-start gap-3 rounded-2xl border-none px-3 py-2 text-left shadow-none', 'bg-muted/45 dark:bg-white/[0.04]', isActive && 'bg-muted/80 dark:bg-white/[0.08]', isPending && !isActive && 'text-muted-foreground/80', @@ -69,19 +69,19 @@ export function DistributionModeSelect({
- + {option.title} - {isPending ? '开发中' : isActive ? '已选中' : '选择'} + {isActive ? '*' : ''}
-

+

{option.description}

diff --git a/frontend/components/common/project/EditDialog.tsx b/frontend/components/common/project/EditDialog.tsx index b897db61..b6576fc6 100644 --- a/frontend/components/common/project/EditDialog.tsx +++ b/frontend/components/common/project/EditDialog.tsx @@ -8,9 +8,8 @@ import {useBulkImport} from '@/hooks/use-bulk-import'; import {useFileUpload} from '@/hooks/use-file-upload'; import {toast} from 'sonner'; import {Button} from '@/components/ui/button'; -import {ScrollArea} from '@/components/ui/scroll-area'; import {Tabs, TabsList, TabsTrigger, TabsContent, TabsContents} from '@/components/animate-ui/radix/tabs'; -import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter} from '@/components/animate-ui/radix/dialog'; +import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogBody} from '@/components/animate-ui/radix/dialog'; import {validateProjectForm, validatePriceString, CURRENCY_LABEL} from '@/components/common/project'; import {ProjectBasicForm} from '@/components/common/project/ProjectBasicForm'; import {BulkImportSection} from '@/components/common/project/BulkImportSection'; @@ -145,7 +144,7 @@ export function EditDialog({ return; } if (!cfgResult.data?.has_config) { - toast.error('请先在"支付设置"中配置 clientID 与 clientSecret'); + toast.error('请先在"支付设置"中配置 Client ID 与 Client Secret'); setLoading(false); return; } @@ -216,15 +215,15 @@ export function EditDialog({ - +
- + {updateSuccess ? '项目更新成功' : '编辑项目'} - + {updateSuccess ? '您的项目已更新成功' : '修改项目信息和设置'}
@@ -250,8 +249,8 @@ export function EditDialog({
{updateSuccess ? ( -
-
+ +
@@ -264,23 +263,23 @@ export function EditDialog({
-
+ ) : ( - - + - - +
-
-
+ - {project.distribution_type !== DistributionType.LOTTERY && ( - - + {project.distribution_type !== DistributionType.LOTTERY && ( +
-
-
- )} -
-
+ + )} + + + )} - + {updateSuccess ? ( @@ -349,7 +347,7 @@ export function EditDialog({ onClick={handleSubmit} disabled={loading} size="sm" - className="min-w-20" + className="rounded-full px-3 text-xs shadow-none" > {loading ? '更新中...' : '更新项目'} diff --git a/frontend/components/common/project/ProjectBasicForm.tsx b/frontend/components/common/project/ProjectBasicForm.tsx index 926f9471..5bca5380 100644 --- a/frontend/components/common/project/ProjectBasicForm.tsx +++ b/frontend/components/common/project/ProjectBasicForm.tsx @@ -69,7 +69,7 @@ export function ProjectBasicForm({ } if (!res.data.has_config) { - setPaymentDialogMessage('请先在支付设置中配置 clientID 与 clientSecret,然后再设置领取消耗积分。'); + setPaymentDialogMessage('请先在支付设置中配置 Client ID 与 Client Secret,然后再设置领取消耗积分。'); setPaymentDialogOpen(true); setPaymentConfigChecked(false); return false; @@ -103,7 +103,7 @@ export function ProjectBasicForm({ return ( <>
-