@@ -18,9 +18,26 @@ const NOTIFICATIONS_STORAGE_KEY = 'kanban-ai-notifications-enabled';
1818const CLICKED_NOTIFICATIONS_STORAGE_KEY = ' kanban-ai-notifications-clicked' ;
1919const COMPACT_MODE_STORAGE_KEY = ' kanban-ai-notifications-compact' ;
2020const DISPLAY_MODE_STORAGE_KEY = ' kanban-ai-notifications-mode' ;
21+ const CURRENT_PROJECT_ONLY_STORAGE_KEY = ' kanban-ai-notifications-current-project-only' ;
2122const notificationsEnabled = ref (true );
2223const clickedNotifications = ref <Set <string >>(new Set ());
2324const compactModeEnabled = ref (false );
25+ const currentProjectOnly = ref (false );
26+
27+ // 项目序号过滤(不存储到 localStorage)
28+ const projectIndexFilter = ref <string | null >(null );
29+
30+ // 项目序号颜色表
31+ const PROJECT_INDEX_COLORS = [
32+ ' #10b981' , // 绿色
33+ ' #3b82f6' , // 蓝色
34+ ' #f59e0b' , // 橙色
35+ ' #8b5cf6' , // 紫色
36+ ' #ec4899' , // 粉色
37+ ' #14b8a6' , // 青色
38+ ' #ef4444' , // 红色
39+ ' #6366f1' , // 靛蓝色
40+ ];
2441
2542type NotificationDisplayMode = ' standard' | ' idle-only' | ' exclude-idle' ;
2643const DISPLAY_MODE_SEQUENCE: NotificationDisplayMode [] = [' standard' , ' idle-only' , ' exclude-idle' ];
@@ -110,6 +127,25 @@ function saveDisplayModeSetting() {
110127 }
111128}
112129
130+ function loadCurrentProjectOnlySetting() {
131+ try {
132+ const stored = localStorage .getItem (CURRENT_PROJECT_ONLY_STORAGE_KEY );
133+ if (stored !== null ) {
134+ currentProjectOnly .value = stored === ' true' ;
135+ }
136+ } catch (error ) {
137+ console .warn (' [AI Notification] Failed to load current project only setting' , error );
138+ }
139+ }
140+
141+ function saveCurrentProjectOnlySetting() {
142+ try {
143+ localStorage .setItem (CURRENT_PROJECT_ONLY_STORAGE_KEY , String (currentProjectOnly .value ));
144+ } catch (error ) {
145+ console .warn (' [AI Notification] Failed to save current project only setting' , error );
146+ }
147+ }
148+
113149function markNotificationsAsRead(notificationIds : string []) {
114150 let changed = false ;
115151 notificationIds .forEach (id => {
@@ -178,6 +214,12 @@ function handleNotificationModeSelect(key: string | number) {
178214 if (typeof key !== ' string' ) {
179215 return ;
180216 }
217+ // 处理"仅当前项目" checkbox
218+ if (key === ' current-project-only' ) {
219+ currentProjectOnly .value = ! currentProjectOnly .value ;
220+ saveCurrentProjectOnlySetting ();
221+ return ;
222+ }
181223 setDisplayMode (key as NotificationDisplayMode );
182224}
183225
@@ -350,22 +392,98 @@ function getDisplayModeLabel(mode: NotificationDisplayMode) {
350392 return t (' terminal.notificationModeAll' );
351393}
352394
353- const notificationModeOptions = computed <DropdownOption []>(() =>
354- DISPLAY_MODE_SEQUENCE .map (mode => ({
395+ const notificationModeOptions = computed <DropdownOption []>(() => [
396+ ... DISPLAY_MODE_SEQUENCE .map (mode => ({
355397 label: getDisplayModeLabel (mode ),
356398 key: mode ,
357- }))
358- );
399+ })),
400+ { type: ' divider' , key: ' d1' },
401+ {
402+ label: ` ${currentProjectOnly .value ? ' ✓ ' : ' ' }${t (' terminal.notificationModeCurrentProjectOnly' )} ` ,
403+ key: ' current-project-only' ,
404+ },
405+ ]);
359406
360407const currentDisplayModeLabel = computed (() => getDisplayModeLabel (notificationDisplayMode .value ));
361408
409+ // 计算项目序号映射(基于 projectId 分配序号和颜色)
410+ const projectIndexMap = computed (() => {
411+ const map = new Map <string , { index: number ; color: string }>();
412+ const seenProjects: string [] = [];
413+
414+ // 按时间顺序遍历通知,收集唯一的 projectId
415+ for (const notification of notifications .value ) {
416+ if (notification .projectId && ! seenProjects .includes (notification .projectId )) {
417+ seenProjects .push (notification .projectId );
418+ }
419+ }
420+
421+ // 为每个项目分配序号和颜色
422+ seenProjects .forEach ((projectId , idx ) => {
423+ map .set (projectId , {
424+ index: idx + 1 ,
425+ color: PROJECT_INDEX_COLORS [idx % PROJECT_INDEX_COLORS .length ],
426+ });
427+ });
428+
429+ return map ;
430+ });
431+
432+ // 获取通知的项目序号信息
433+ function getProjectIndex(notification : NotificationItem ) {
434+ return projectIndexMap .value .get (notification .projectId );
435+ }
436+
437+ // 点击项目序号切换过滤
438+ function toggleProjectFilter(projectId : string , event : MouseEvent ) {
439+ event .stopPropagation ();
440+ if (projectIndexFilter .value === projectId ) {
441+ projectIndexFilter .value = null ;
442+ } else {
443+ projectIndexFilter .value = projectId ;
444+ }
445+ }
446+
447+ const currentProjectId = computed (() => {
448+ const id = currentRoute .params .id ;
449+ return typeof id === ' string' ? id : ' ' ;
450+ });
451+
362452const filteredNotifications = computed (() => {
363453 if (! notificationsEnabled .value ) {
364454 return [];
365455 }
366- return notifications .value .filter (notification => matchesDisplayMode (notification ));
456+ return notifications .value .filter (notification => {
457+ // 先检查显示模式过滤
458+ if (! matchesDisplayMode (notification )) {
459+ return false ;
460+ }
461+ // 如果启用了"仅当前项目",过滤非当前项目的通知
462+ if (currentProjectOnly .value && currentProjectId .value ) {
463+ if (notification .projectId !== currentProjectId .value ) {
464+ return false ;
465+ }
466+ }
467+ // 如果启用了项目序号过滤,只显示该项目的通知
468+ if (projectIndexFilter .value !== null ) {
469+ if (notification .projectId !== projectIndexFilter .value ) {
470+ return false ;
471+ }
472+ }
473+ return true ;
474+ });
367475});
368476
477+ // 监听过滤后的通知数量,如果为0则自动取消项目序号过滤
478+ watch (
479+ () => filteredNotifications .value .length ,
480+ (newLength ) => {
481+ if (newLength === 0 && projectIndexFilter .value !== null ) {
482+ projectIndexFilter .value = null ;
483+ }
484+ }
485+ );
486+
369487function matchesDisplayMode(notification : NotificationItem ) {
370488 if (notificationDisplayMode .value === ' idle-only' ) {
371489 return isIdleNotification (notification );
@@ -1051,6 +1169,7 @@ onMounted(() => {
10511169 loadClickedNotifications ();
10521170 loadCompactModeSetting ();
10531171 loadDisplayModeSetting ();
1172+ loadCurrentProjectOnlySetting ();
10541173
10551174 terminalStore .emitter .on (' ai:completed' , handleAICompletion );
10561175 terminalStore .emitter .on (' ai:approval-needed' , handleAIApproval );
@@ -1245,13 +1364,33 @@ watch(
12451364 <div
12461365 v-for =" notification in filteredNotifications"
12471366 :key =" notification.id"
1248- :class =" [
1249- 'notification-item',
1250- getNotificationClass(notification),
1251- { 'notification-clicked': isNotificationClicked(notification.id) },
1252- ]"
1253- @click =" handleNotificationClick(notification)"
1367+ class =" notification-row"
12541368 >
1369+ <!-- 项目序号标签(在卡片外面,仅多个项目时显示) -->
1370+ <button
1371+ v-if =" projectIndexMap.size > 1 && getProjectIndex(notification)"
1372+ class =" project-index-badge"
1373+ :class =" { 'is-filtered': projectIndexFilter === notification.projectId }"
1374+ :style =" {
1375+ '--badge-color': getProjectIndex(notification)?.color,
1376+ }"
1377+ @click =" toggleProjectFilter(notification.projectId, $event)"
1378+ :title ="
1379+ projectIndexFilter === notification.projectId
1380+ ? t('terminal.clearProjectFilter')
1381+ : t('terminal.filterByProject')
1382+ "
1383+ >
1384+ {{ getProjectIndex(notification)?.index }}
1385+ </button >
1386+ <div
1387+ :class =" [
1388+ 'notification-item',
1389+ getNotificationClass(notification),
1390+ { 'notification-clicked': isNotificationClicked(notification.id) },
1391+ ]"
1392+ @click =" handleNotificationClick(notification)"
1393+ >
12551394 <div class =" notification-content" >
12561395 <div v-if =" !compactModeEnabled" class =" notification-header" >
12571396 <span
@@ -1316,6 +1455,7 @@ watch(
13161455 >
13171456 ×
13181457 </button >
1458+ </div >
13191459 </div >
13201460 </transition-group >
13211461 </div >
@@ -1460,8 +1600,8 @@ watch(
14601600 display : flex ;
14611601 flex-direction : column ;
14621602 gap : 6px ;
1463- width : min (320 px , calc (100vw - 32px ));
1464- max-width : 360 px ;
1603+ width : min (345 px , calc (100vw - 32px ));
1604+ max-width : 380 px ;
14651605}
14661606
14671607.notification-list.is-compact {
@@ -1477,22 +1617,21 @@ watch(
14771617 border-radius : 12px ;
14781618 box-shadow : 0 12px 28px rgba (15 , 23 , 42 , 0.18 );
14791619 cursor : pointer ;
1480- pointer-events : auto ;
14811620 transition :
14821621 transform 0.2s ease ,
14831622 box-shadow 0.2s ease ;
14841623 border : 1px solid rgba (15 , 23 , 42 , 0.08 );
14851624 border-left : 4px solid transparent ;
1486- min-width : 320 px ;
1487- width : 100 % ;
1625+ min-width : 280 px ;
1626+ flex : 1 ;
14881627 backdrop-filter : blur (12px );
14891628 -webkit-backdrop-filter : blur (12px );
14901629}
14911630
14921631.notification-list.is-compact .notification-item {
14931632 padding : 6px 10px ;
14941633 border-radius : 6px ;
1495- min-width : 280 px ;
1634+ min-width : 240 px ;
14961635 gap : 6px ;
14971636 align-items : center ;
14981637}
@@ -1704,6 +1843,55 @@ watch(
17041843 line-height : 1.2 ;
17051844}
17061845
1846+ /* 通知行容器(包含序号和卡片) */
1847+ .notification-row {
1848+ display : flex ;
1849+ align-items : flex-start ;
1850+ gap : 6px ;
1851+ pointer-events : auto ;
1852+ }
1853+
1854+ /* 项目序号标签样式(在卡片外部) */
1855+ .project-index-badge {
1856+ flex-shrink : 0 ;
1857+ width : 22px ;
1858+ height : 22px ;
1859+ border-radius : 50% ;
1860+ background-color : var (--badge-color , #3b82f6 );
1861+ color : #fff ;
1862+ font-size : 12px ;
1863+ font-weight : 600 ;
1864+ display : flex ;
1865+ align-items : center ;
1866+ justify-content : center ;
1867+ cursor : pointer ;
1868+ border : 2px solid transparent ;
1869+ transition :
1870+ transform 0.15s ease ,
1871+ box-shadow 0.15s ease ,
1872+ border-color 0.15s ease ;
1873+ margin-top : 10px ;
1874+ }
1875+
1876+ .project-index-badge :hover {
1877+ transform : scale (1.1 );
1878+ box-shadow : 0 2px 8px rgba (0 , 0 , 0 , 0.2 );
1879+ }
1880+
1881+ .project-index-badge.is-filtered {
1882+ border-color : #fff ;
1883+ box-shadow :
1884+ 0 0 0 2px var (--badge-color , #3b82f6 ),
1885+ 0 2px 8px rgba (0 , 0 , 0 , 0.25 );
1886+ }
1887+
1888+ .notification-list.is-compact .project-index-badge {
1889+ width : 18px ;
1890+ height : 18px ;
1891+ font-size : 10px ;
1892+ margin-top : 4px ;
1893+ }
1894+
17071895.notification-close {
17081896 flex-shrink : 0 ;
17091897 width : 20px ;
0 commit comments