feat: 登录注册表单体验优化、工作台滚动按钮交互增强、主题层视觉精修
ProfilePage.tsx(登录/注册表单优化): - 新增邮箱格式、手机号、密码强度前端校验逻辑 - 输入框内联校验提示(CheckCircleFilled图标+绿色文字) - 登录态展示区增加平台能力Stats(Studio/Assets/Team) - 表单增加kicker标签区分"账户登录"/"新用户注册" - 手机验证码tab标签精简为"手机" WorkbenchPage.tsx(工作台滚动交互增强): - 新增滚动操作按钮显隐状态管理(scrollActionsVisible/direction) - 滚动时自动展示上下滚动按钮,950ms后自动隐藏 - scrollMessagesToLatest触发时间接展示滚动按钮 - 组件卸载时清理定时器避免内存泄漏 dark-green.css(主题层视觉精修): - 工作台激活态页面背景增加微光渐变和径向光晕 - 消息表面区域增加内边距和scrollbar-gutter稳定布局 - 消息列表最大宽度约束为1040px,左右padding自适应 - 消息气泡增加边框、背景、阴影层次感 - AI助手气泡与用户气泡差异化背景色 - 头像增加微边框和accent色区分 - 作者标签字号和颜色精细调整
This commit is contained in:
@@ -236,6 +236,7 @@ function WorkbenchPage({
|
||||
const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks());
|
||||
const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||
const lastScrollTopRef = useRef(0);
|
||||
const scrollActionsHideTimerRef = useRef<number | null>(null);
|
||||
const shouldFollowNewMessagesRef = useRef(true);
|
||||
const pendingScrollToLatestRef = useRef(true);
|
||||
const renderedMessageIdsRef = useRef<string[]>([]);
|
||||
@@ -273,6 +274,8 @@ function WorkbenchPage({
|
||||
const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
|
||||
const [mentionActiveIndex, setMentionActiveIndex] = useState(0);
|
||||
const [composerHidden, setComposerHidden] = useState(false);
|
||||
const [scrollActionsVisible, setScrollActionsVisible] = useState(false);
|
||||
const [scrollActionDirection, setScrollActionDirection] = useState<"top" | "bottom" | null>(null);
|
||||
const [workspaceStarted, setWorkspaceStarted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -441,6 +444,27 @@ function WorkbenchPage({
|
||||
"--accent-glow": `0 0 24px rgba(${accentRgb}, 0.22)`,
|
||||
} as CSSProperties;
|
||||
|
||||
const revealScrollActionsTemporarily = useCallback((direction: "top" | "bottom") => {
|
||||
setScrollActionDirection(direction);
|
||||
setScrollActionsVisible(true);
|
||||
if (scrollActionsHideTimerRef.current !== null) {
|
||||
window.clearTimeout(scrollActionsHideTimerRef.current);
|
||||
}
|
||||
scrollActionsHideTimerRef.current = window.setTimeout(() => {
|
||||
setScrollActionsVisible(false);
|
||||
setScrollActionDirection(null);
|
||||
scrollActionsHideTimerRef.current = null;
|
||||
}, 950);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (scrollActionsHideTimerRef.current !== null) {
|
||||
window.clearTimeout(scrollActionsHideTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scrollMessagesToLatest = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||
const scroll = () => {
|
||||
const surface = messagesSurfaceRef.current;
|
||||
@@ -451,6 +475,7 @@ function WorkbenchPage({
|
||||
|
||||
setComposerHidden(false);
|
||||
shouldFollowNewMessagesRef.current = true;
|
||||
revealScrollActionsTemporarily("bottom");
|
||||
surface.scrollTo({ top: surface.scrollHeight, behavior });
|
||||
lastScrollTopRef.current = surface.scrollTop;
|
||||
};
|
||||
@@ -459,7 +484,7 @@ function WorkbenchPage({
|
||||
scroll();
|
||||
window.setTimeout(scroll, 80);
|
||||
});
|
||||
}, []);
|
||||
}, [revealScrollActionsTemporarily]);
|
||||
|
||||
const imageSettingGroups = useMemo<WorkbenchFieldGroup[]>(
|
||||
() => [
|
||||
@@ -1373,6 +1398,9 @@ function WorkbenchPage({
|
||||
const delta = top - lastScrollTopRef.current;
|
||||
const atTop = top <= edgeThreshold;
|
||||
const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
|
||||
if (surface.scrollHeight > surface.clientHeight + edgeThreshold && Math.abs(delta) > 1) {
|
||||
revealScrollActionsTemporarily(delta > 0 ? "bottom" : "top");
|
||||
}
|
||||
shouldFollowNewMessagesRef.current = atBottom;
|
||||
if (atTop || atBottom) {
|
||||
setComposerHidden(false);
|
||||
@@ -1384,7 +1412,7 @@ function WorkbenchPage({
|
||||
|
||||
surface.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => surface.removeEventListener("scroll", handleScroll);
|
||||
}, [hasActivatedWorkspace]);
|
||||
}, [hasActivatedWorkspace, revealScrollActionsTemporarily]);
|
||||
|
||||
const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => {
|
||||
const surface = messagesSurfaceRef.current;
|
||||
@@ -1392,8 +1420,9 @@ function WorkbenchPage({
|
||||
|
||||
const top = direction === "top" ? 0 : surface.scrollHeight;
|
||||
setComposerHidden(false);
|
||||
revealScrollActionsTemporarily(direction);
|
||||
surface.scrollTo({ top, behavior: "smooth" });
|
||||
}, []);
|
||||
}, [revealScrollActionsTemporarily]);
|
||||
|
||||
const closeToolbarMenus = () => setToolbarMenuId(null);
|
||||
const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => {
|
||||
@@ -3022,10 +3051,13 @@ function WorkbenchPage({
|
||||
{renderComposerToolbar(false, isGenerating)}
|
||||
</div>
|
||||
</section>
|
||||
<div className="wb-chat-scroll-actions" aria-label="聊天滚动">
|
||||
<div
|
||||
className={`wb-chat-scroll-actions${scrollActionsVisible ? " is-visible" : ""}${scrollActionDirection ? ` is-${scrollActionDirection}` : ""}`}
|
||||
aria-label="聊天滚动"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="wb-chat-scroll-actions__button"
|
||||
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--top"
|
||||
title="返回聊天顶部"
|
||||
aria-label="返回聊天顶部"
|
||||
onClick={() => scrollMessagesSurface("top")}
|
||||
@@ -3034,7 +3066,7 @@ function WorkbenchPage({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="wb-chat-scroll-actions__button"
|
||||
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--bottom"
|
||||
title="到达聊天底部"
|
||||
aria-label="到达聊天底部"
|
||||
onClick={() => scrollMessagesSurface("bottom")}
|
||||
|
||||
Reference in New Issue
Block a user