Skip to content

refactor: 重构项目绑定弹窗组件#157

Merged
Bowl42 merged 1 commit intomainfrom
refactor/force-project-dialog
Jan 25, 2026
Merged

refactor: 重构项目绑定弹窗组件#157
Bowl42 merged 1 commit intomainfrom
refactor/force-project-dialog

Conversation

@Bowl42
Copy link
Collaborator

@Bowl42 Bowl42 commented Jan 25, 2026

Summary

  • 重构 ForceProjectDialog 组件,拆分为多个子组件提高可维护性
  • 提取 useCountdown hook 用于倒计时逻辑复用
  • 优化用户体验:点击项目直接绑定,无需确认按钮
  • 在请求列表和详情页添加项目绑定状态显示和操作

Changes

  • force-project-dialog.tsx: 拆分为 SessionInfo, CountdownTimer, ProjectSelector 子组件
  • use-countdown.ts: 新增倒计时 hook
  • requests/index.tsx: 添加等待绑定状态显示 (amber 样式)
  • requests/detail.tsx: 添加项目绑定横幅,支持重新绑定
  • app-layout.tsx: 将弹窗移至 SidebarProvider 外部,修复 z-index 问题
  • locales/*.json: 添加国际化键

Test plan

  • 启用强制项目绑定设置
  • 新建会话时弹窗正常显示
  • 点击项目按钮直接绑定成功
  • 请求列表中等待绑定的请求显示 amber 样式
  • 请求详情页显示绑定横幅并可重新绑定

🤖 Generated with Claude Code

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加项目绑定工作流:用户现可在请求详情中为待处理的请求选择并绑定项目
    • 新增"待绑定"状态,在请求列表中以琥珀色高亮显示
    • 新增倒计时提示:用户在项目选择框中可见剩余时间倒数
  • 用户界面改进

    • 优化项目选择对话框,新增会话信息展示和倒计时计时器组件
    • 改进了绑定状态的视觉反馈,包括加载状态和成功/失败提示
  • 国际化

    • 为项目绑定流程添加多语言支持,包括拒绝、确认绑定和相关提示信息

✏️ Tip: You can customize this high-level summary in your review settings.

- 拆分 ForceProjectDialog 为多个子组件 (SessionInfo, CountdownTimer, ProjectSelector)
- 提取 useCountdown hook 用于倒计时逻辑
- 点击项目直接绑定,无需确认按钮
- 优化项目按钮样式,更加明显 (amber 边框和背景)
- 在请求列表中为等待绑定的请求显示 "等待绑定" 状态
- 在请求详情页添加项目绑定横幅,支持重新绑定
- 完善国际化支持
- 修复弹窗 z-index 问题,移至 SidebarProvider 外部

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

📝 Walkthrough

概览

本PR重构了ForceProjectDialog的倒计时逻辑,引入可复用的useCountdown钩子和专门的超时处理器,提取独立的子组件(SessionInfo、CountdownTimer、ProjectSelector),并在请求详情页面和列表页面增加项目绑定功能,扩展了国际化支持。

变更

聚类 / 文件 变更摘要
倒计时钩子与对话框重构
web/src/hooks/use-countdown.ts, web/src/components/force-project-dialog.tsx, web/src/components/layout/app-layout.tsx
新增useCountdown钩子提供可复用的倒计时功能;force-project-dialog重构为子组件架构(SessionInfo、CountdownTimer、ProjectSelector),移除自定义间隔状态管理;更新app-layout结构,将ForceProjectDialog移至SidebarProvider外层以优化z-index层级
项目绑定功能实现
web/src/pages/requests/detail.tsx, web/src/pages/requests/index.tsx
详情页添加项目绑定操作流程及用户反馈机制;列表页支持待绑定状态识别和样式展现,扩展RequestStatusBadge和LogRow组件以传递绑定上下文
国际化扩展
web/src/locales/en.json, web/src/locales/zh.json
新增requests.status.pendingBinding及sessions命名空间下的11个翻译键(拒绝、绑定、超时警告等操作和提示文案)

代码审查工作量评估

🎯 4(复杂) | ⏱️ ~45 分钟

可能相关的PR

建议的审核者

  • awsl233777
  • whhjdi

诗歌

🐰 倒计时的秒针滑落,
项目在对话框中选择,
子组件们各司其职,
国际化的文字闪闪发光,
绑定流程如丝般顺滑~ ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确总结了主要变更内容:重构ForceProjectDialog项目绑定弹窗组件,与提交涵盖的所有文件变更(子组件拆分、倒计时hook提取、绑定流程优化)完全相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@web/src/components/force-project-dialog.tsx`:
- Around line 178-182: 当前超时处理器 handleTimeout 只调用 onClose() 导致行为与翻译
sessions.timeoutWarning 不一致;在 handleTimeout 中检测到 event 存在时应调用
rejectSession(event)(然后再调用 onClose()),以在超时自动拒绝会话;如果不想自动拒绝则相反地更新
sessions.timeoutWarning 翻译文案以准确反映“仅关闭弹窗不拒绝请求”行为。

In `@web/src/hooks/use-countdown.ts`:
- Around line 58-62: The effect in use-countdown uses remainingTime and
onComplete and can re-run and call onComplete again if onComplete identity
changes while remainingTime is 0; fix this by adding a ref flag (e.g.,
calledRef) inside the useCountdown hook to ensure onComplete is invoked only
once per countdown: check remainingTime === 0 && !calledRef.current before
calling onComplete, set calledRef.current = true after calling, and reset
calledRef.current = false when the countdown restarts (e.g., when remainingTime
becomes > 0 or when the start/reset function is invoked); keep the useEffect
dependency array as [remainingTime, onComplete] but rely on the ref to prevent
duplicate calls.
🧹 Nitpick comments (4)
web/src/hooks/use-countdown.ts (1)

41-56: 依赖数组中包含 remainingTime 会导致每秒重建 interval。

当前实现在每次 remainingTime 变化时都会清除并重新创建 interval,虽然功能正确,但会产生不必要的开销。可以使用 useRef 来避免将 remainingTime 加入依赖数组。

♻️ 建议的优化方案
+import { useEffect, useState, useCallback, useRef } from 'react';
-import { useEffect, useState, useCallback } from 'react';

 export function useCountdown({
   initialSeconds,
   onComplete,
   autoStart = true,
 }: UseCountdownOptions): UseCountdownReturn {
   const [remainingTime, setRemainingTime] = useState(initialSeconds);
   const [isRunning, setIsRunning] = useState(autoStart);
+  const onCompleteRef = useRef(onComplete);
+
+  useEffect(() => {
+    onCompleteRef.current = onComplete;
+  }, [onComplete]);

   // ... reset, start, stop callbacks ...

   useEffect(() => {
-    if (!isRunning || remainingTime <= 0) return;
+    if (!isRunning) return;

     const interval = setInterval(() => {
       setRemainingTime((prev) => {
         if (prev <= 1) {
           clearInterval(interval);
           setIsRunning(false);
+          onCompleteRef.current?.();
           return 0;
         }
         return prev - 1;
       });
     }, 1000);

     return () => clearInterval(interval);
-  }, [isRunning, remainingTime]);
+  }, [isRunning]);

-  useEffect(() => {
-    if (remainingTime === 0 && onComplete) {
-      onComplete();
-    }
-  }, [remainingTime, onComplete]);

   return { remainingTime, isRunning, reset, start, stop };
 }
web/src/pages/requests/detail.tsx (2)

55-58: needsProjectBinding 的条件检查可以更加健壮。

当前条件 !request.projectID || request.projectID === 0 是正确的,但考虑到 !request.projectID 已经涵盖了 0nullundefined 的情况,|| request.projectID === 0 是冗余的。

♻️ 简化条件表达式
   const needsProjectBinding = request && (
     request.status === 'REJECTED' ||
-    (request.status === 'PENDING' && forceProjectBinding && (!request.projectID || request.projectID === 0))
+    (request.status === 'PENDING' && forceProjectBinding && !request.projectID)
   );

198-240: 绑定横幅 UI 实现良好,但可考虑添加 toast 反馈。

当前实现通过按钮状态变化(加载中 → 成功图标)来显示绑定结果。建议结合 toast 通知,特别是绑定失败时,让用户获得更明确的错误反馈。

另外,Line 86-88 的错误处理只是 console.error,用户无法感知失败原因:

} catch (error) {
  console.error('Failed to bind project:', error);
  setSelectedProjectId(0);
  // 考虑添加: toast.error(t('sessions.bindFailed'));
}
web/src/pages/requests/index.tsx (1)

342-365: 可选:抽出 isPendingBinding 逻辑避免重复。
当前在 RequestStatusBadge 与 LogRow 各自计算,后续规则变化需要双改;可抽成 helper 或直接传入 boolean。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21898a9 and a3c21bf.

📒 Files selected for processing (7)
  • web/src/components/force-project-dialog.tsx
  • web/src/components/layout/app-layout.tsx
  • web/src/hooks/use-countdown.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/src/pages/requests/detail.tsx
  • web/src/pages/requests/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/hooks/use-countdown.ts (1)
launcher/script.js (1)
  • seconds (121-121)
🔇 Additional comments (11)
web/src/components/layout/app-layout.tsx (1)

15-38: LGTM! 将 ForceProjectDialog 移至 SidebarProvider 外部是修复 z-index 问题的正确做法。

使用 Fragment 包装并将弹窗渲染为 SidebarProvider 的兄弟元素,确保弹窗不会被侧边栏的层叠上下文影响。代码结构清晰,注释说明了这样做的原因。

web/src/locales/zh.json (1)

142-142: LGTM! 新增的国际化键与英文版本保持一致。

翻译内容准确且符合中文习惯用法,键名结构与 en.json 保持同步。

Also applies to: 292-301

web/src/pages/requests/detail.tsx (1)

72-90: LGTM! 项目绑定处理逻辑完整。

错误处理恰当,失败时重置 selectedProjectId,成功后刷新请求数据并显示反馈。useCallback 的依赖数组也是正确的。

web/src/locales/en.json (1)

142-142: LGTM! 国际化键结构清晰,命名一致。

新增的状态和会话相关翻译文案准确描述了功能场景。

Also applies to: 292-301

web/src/components/force-project-dialog.tsx (2)

35-163: LGTM! 子组件拆分提高了可维护性。

SessionInfoCountdownTimerProjectSelector 提取为独立组件是良好的重构实践,每个组件职责单一,便于测试和复用。类型定义也很清晰。


255-261: 点击项目直接绑定的交互设计符合 PR 目标。

ProjectSelectoronSelect 直接调用 handleConfirm 实现了"点击项目直接绑定,无需确认按钮"的需求。disabled 属性正确地在任一操作进行时禁用所有按钮,防止重复操作。

web/src/pages/requests/index.tsx (5)

95-96: 强制绑定开关读取清晰。
将设置值归一为布尔后再下游使用,逻辑直观。


280-281: 向 LogRow 透传开关到位。
确保行级渲染能够感知强制绑定配置。


465-492: 行级 pendingBinding 判断合理。
状态/项目 ID 组合判定与需求一致。


571-571: 绑定等待态的样式区分清晰。
hover 与 marquee 的抑制条件合理,视觉层级清楚。

Also applies to: 579-585, 587-590


665-669: 状态徽标参数补齐。
确保徽标能呈现绑定等待态。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +178 to +182
const handleTimeout = useCallback(() => {
if (event) {
onClose();
}
}, [event, onClose]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

超时处理与警告文案不一致。

警告文案 sessions.timeoutWarning 说明"如果未在规定时间内选择项目,请求将被拒绝",但 handleTimeout 只是关闭弹窗而没有调用 rejectSession。这可能导致用户体验与预期不符。

🐛 建议在超时时自动拒绝会话
   const handleTimeout = useCallback(() => {
     if (event) {
+      rejectSession.mutateAsync(event.sessionID).catch((error) => {
+        console.error('Failed to reject session on timeout:', error);
+      });
       onClose();
     }
-  }, [event, onClose]);
+  }, [event, onClose, rejectSession]);

或者,如果超时只是关闭弹窗而不拒绝请求,则需要更新 timeoutWarning 的翻译文案以准确描述行为。

🤖 Prompt for AI Agents
In `@web/src/components/force-project-dialog.tsx` around lines 178 - 182, 当前超时处理器
handleTimeout 只调用 onClose() 导致行为与翻译 sessions.timeoutWarning 不一致;在 handleTimeout
中检测到 event 存在时应调用 rejectSession(event)(然后再调用
onClose()),以在超时自动拒绝会话;如果不想自动拒绝则相反地更新 sessions.timeoutWarning
翻译文案以准确反映“仅关闭弹窗不拒绝请求”行为。

Comment on lines +58 to +62
useEffect(() => {
if (remainingTime === 0 && onComplete) {
onComplete();
}
}, [remainingTime, onComplete]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onComplete 可能在 remainingTime 已为 0 时被重复调用。

如果 onComplete 回调函数引用发生变化(例如父组件重新渲染),且此时 remainingTime 已经是 0,这个 effect 会再次触发 onComplete

🐛 建议使用 ref 或添加调用标记
+  const hasCompletedRef = useRef(false);

   useEffect(() => {
-    if (remainingTime === 0 && onComplete) {
+    if (remainingTime === 0 && onComplete && !hasCompletedRef.current) {
+      hasCompletedRef.current = true;
       onComplete();
     }
   }, [remainingTime, onComplete]);

+  // 在 reset 中重置标记
   const reset = useCallback(
     (seconds?: number) => {
       setRemainingTime(seconds ?? initialSeconds);
       setIsRunning(autoStart);
+      hasCompletedRef.current = false;
     },
     [initialSeconds, autoStart],
   );
🤖 Prompt for AI Agents
In `@web/src/hooks/use-countdown.ts` around lines 58 - 62, The effect in
use-countdown uses remainingTime and onComplete and can re-run and call
onComplete again if onComplete identity changes while remainingTime is 0; fix
this by adding a ref flag (e.g., calledRef) inside the useCountdown hook to
ensure onComplete is invoked only once per countdown: check remainingTime === 0
&& !calledRef.current before calling onComplete, set calledRef.current = true
after calling, and reset calledRef.current = false when the countdown restarts
(e.g., when remainingTime becomes > 0 or when the start/reset function is
invoked); keep the useEffect dependency array as [remainingTime, onComplete] but
rely on the ref to prevent duplicate calls.

@Bowl42 Bowl42 merged commit f57741c into main Jan 25, 2026
2 checks passed
@Bowl42 Bowl42 deleted the refactor/force-project-dialog branch January 25, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants