Skip to content

Commit beb4523

Browse files
committed
feat: 为提醒卡片增加项目标号功能
1 parent 631bb7b commit beb4523

File tree

3 files changed

+211
-17
lines changed

3 files changed

+211
-17
lines changed

ui/src/components/terminal/AINotificationBar.vue

Lines changed: 205 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,26 @@ const NOTIFICATIONS_STORAGE_KEY = 'kanban-ai-notifications-enabled';
1818
const CLICKED_NOTIFICATIONS_STORAGE_KEY = 'kanban-ai-notifications-clicked';
1919
const COMPACT_MODE_STORAGE_KEY = 'kanban-ai-notifications-compact';
2020
const DISPLAY_MODE_STORAGE_KEY = 'kanban-ai-notifications-mode';
21+
const CURRENT_PROJECT_ONLY_STORAGE_KEY = 'kanban-ai-notifications-current-project-only';
2122
const notificationsEnabled = ref(true);
2223
const clickedNotifications = ref<Set<string>>(new Set());
2324
const 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
2542
type NotificationDisplayMode = 'standard' | 'idle-only' | 'exclude-idle';
2643
const 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+
113149
function 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
360407
const 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+
362452
const 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+
369487
function 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(320px, calc(100vw - 32px));
1464-
max-width: 360px;
1603+
width: min(345px, calc(100vw - 32px));
1604+
max-width: 380px;
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: 320px;
1487-
width: 100%;
1625+
min-width: 280px;
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: 280px;
1634+
min-width: 240px;
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;

ui/src/i18n/locales/en-US.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ export default {
444444
notificationModeAll: 'All',
445445
notificationModeIdleOnly: 'Idle Only',
446446
notificationModeExcludeIdle: 'Exclude Idle',
447+
notificationModeCurrentProjectOnly: 'Current Project Only',
447448
notificationModeCycleTooltip: 'Click to cycle through modes',
448449
notificationModeMenuTooltip: 'Select mode from dropdown',
449450
timeJustNow: 'just now',
@@ -473,6 +474,8 @@ export default {
473474
showUserMessagesOnly: 'Show my messages only',
474475
prevUserMessage: 'Previous message',
475476
nextUserMessage: 'Next message',
477+
filterByProject: 'Click to filter by this project',
478+
clearProjectFilter: 'Click to clear filter',
476479
},
477480
notepad: {
478481
global: 'Global',

ui/src/i18n/locales/zh-CN.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ export default {
444444
notificationModeAll: '全部',
445445
notificationModeIdleOnly: '仅空闲',
446446
notificationModeExcludeIdle: '排除空闲',
447+
notificationModeCurrentProjectOnly: '仅当前项目',
447448
notificationModeCycleTooltip: '点击在模式间切换',
448449
notificationModeMenuTooltip: '从下拉菜单选择模式',
449450
timeJustNow: '刚刚',
@@ -473,6 +474,8 @@ export default {
473474
showUserMessagesOnly: '只看我的发言',
474475
prevUserMessage: '上一条发言',
475476
nextUserMessage: '下一条发言',
477+
filterByProject: '点击筛选该项目',
478+
clearProjectFilter: '点击取消筛选',
476479
},
477480
notepad: {
478481
global: '全局',

0 commit comments

Comments
 (0)