From a9d54e7c2b11e5e5393005aafddbaf749617c595 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 7 Aug 2026 11:28:05 +0800 Subject: [PATCH] feat(frontend): enhance ProfilePage with language selection and scrollable sections Add language selection functionality and implement scrollable sections for the ProfilePage. Introduce a new SectionAnchorTabs component for navigation between Webhook and Language sections, improving user experience and organization of settings. --- frontend/src/components/pages/ProfilePage.tsx | 436 +++++++++++------- 1 file changed, 268 insertions(+), 168 deletions(-) diff --git a/frontend/src/components/pages/ProfilePage.tsx b/frontend/src/components/pages/ProfilePage.tsx index 3bd71db..43a13d7 100644 --- a/frontend/src/components/pages/ProfilePage.tsx +++ b/frontend/src/components/pages/ProfilePage.tsx @@ -5,22 +5,35 @@ import { CheckCircle2, Eye, EyeOff, - Globe2, + Languages, Loader2, - LockKeyhole, Send, Webhook, } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { SectionAnchorTabs } from "@/components/editor/section-anchor-tabs"; import { SectionCard } from "@/components/editor/section-card"; import { ListPageLayout } from "@/components/layout/list-page-layout"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; type TestStatus = "idle" | "loading" | "success" | "error"; +type ProfileSectionId = "webhook" | "language"; + +const profileSections = [ + { id: "webhook", label: "Webhook" }, + { id: "language", label: "语言" }, +] as const; + +const LANGUAGE_OPTIONS = [{ value: "zh-CN", label: "中文" }] as const; function mockWebhookPayload() { return { @@ -46,14 +59,111 @@ function isHttpsUrl(value: string): boolean { } } +function getAppScrollContainer(): HTMLElement | null { + return document.querySelector(".app-content"); +} + export function ProfilePage() { - const [enabled, setEnabled] = useState(true); const [url, setUrl] = useState(""); const [secret, setSecret] = useState(""); const [secretVisible, setSecretVisible] = useState(false); const [testStatus, setTestStatus] = useState("idle"); const [testMessage, setTestMessage] = useState(""); const [payload, setPayload] = useState | null>(null); + const [language, setLanguage] = useState("zh-CN"); + const [activeSection, setActiveSection] = + useState("webhook"); + + const sectionRefs = useRef>({ + webhook: null, + language: null, + }); + const selectedAnchorRef = useRef(null); + + useEffect(() => { + const scrollContainer = getAppScrollContainer(); + if (!scrollContainer) return; + + let animationFrame = 0; + + function updateActiveSection() { + if (selectedAnchorRef.current) { + setActiveSection(selectedAnchorRef.current); + return; + } + + const containerTop = scrollContainer!.getBoundingClientRect().top; + const activationLine = containerTop + 24; + let nextSection: ProfileSectionId = profileSections[0].id; + + for (const section of profileSections) { + const element = sectionRefs.current[section.id]; + if (element && element.getBoundingClientRect().top <= activationLine) { + nextSection = section.id; + } + } + + const reachedBottom = + scrollContainer!.scrollHeight > scrollContainer!.clientHeight + 8 && + scrollContainer!.scrollHeight - + scrollContainer!.scrollTop - + scrollContainer!.clientHeight < + 8; + if (reachedBottom) { + nextSection = profileSections[profileSections.length - 1].id; + } + + setActiveSection((current) => + current === nextSection ? current : nextSection, + ); + } + + function scheduleUpdate() { + window.cancelAnimationFrame(animationFrame); + animationFrame = window.requestAnimationFrame(updateActiveSection); + } + + function releaseSelectedAnchor() { + selectedAnchorRef.current = null; + scheduleUpdate(); + } + + scheduleUpdate(); + scrollContainer.addEventListener("scroll", scheduleUpdate, { + passive: true, + }); + scrollContainer.addEventListener("wheel", releaseSelectedAnchor, { + passive: true, + }); + scrollContainer.addEventListener("touchstart", releaseSelectedAnchor, { + passive: true, + }); + window.addEventListener("resize", scheduleUpdate); + + return () => { + window.cancelAnimationFrame(animationFrame); + scrollContainer.removeEventListener("scroll", scheduleUpdate); + scrollContainer.removeEventListener("wheel", releaseSelectedAnchor); + scrollContainer.removeEventListener("touchstart", releaseSelectedAnchor); + window.removeEventListener("resize", scheduleUpdate); + }; + }, []); + + function scrollToSection(sectionId: ProfileSectionId) { + const scrollContainer = getAppScrollContainer(); + const section = sectionRefs.current[sectionId]; + if (!scrollContainer || !section) return; + + const containerTop = scrollContainer.getBoundingClientRect().top; + const sectionTop = section.getBoundingClientRect().top; + + selectedAnchorRef.current = sectionId; + setActiveSection(sectionId); + scrollContainer.scrollTo({ + top: scrollContainer.scrollTop + sectionTop - containerTop, + behavior: "smooth", + }); + } async function sendTestEvent() { if (!isHttpsUrl(url.trim())) { @@ -75,178 +185,168 @@ export function ProfilePage() { return ( -
- - - } - title="通话分析 Webhook" - description="站点级事件出口;后端接入后将在分析完成时执行 HTTP POST" - className="self-start" +
+
{ + sectionRefs.current.webhook = element; + }} + className="scroll-mt-3" > -
-
-
- - 分析完成事件 + } + title="通话分析 Webhook" + description="站点级事件出口;后端接入后将在分析完成时执行 HTTP POST" + > +
+
-

- 事件类型:call.analysis.completed -

-
- -
- -
- - -
+ +
{ + sectionRefs.current.language = element; + }} + className="scroll-mt-3" + > + } + title="语言" + description="选择控制台界面语言;当前仅提供中文" + > + -
- -
- - - 等待后端设置 API -
- - {testMessage && ( -
- {testStatus === "success" && ( - - )} - {testMessage} -
- )} - - {payload && ( -
-
- - - Mock payload - -
-
-                {JSON.stringify(payload, null, 2)}
-              
-
- )} -
+ +
);