优化了显示
parent
2dbff78698
commit
4ef3c92d65
|
|
@ -82,6 +82,33 @@ async def get_unread_count(
|
||||||
return success_response(data={"unread_count": 0})
|
return success_response(data={"unread_count": 0})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/unread-by-project", response_model=dict)
|
||||||
|
async def get_unread_by_project(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""按项目统计未读通知数量(与消息通知一致),返回 {project_id: count}"""
|
||||||
|
try:
|
||||||
|
counts = await notification_service.get_unread_count_by_project(current_user.id)
|
||||||
|
return success_response(data={"unread_by_project": counts})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching unread by project: {e}")
|
||||||
|
return success_response(data={"unread_by_project": {}})
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/read-project/{project_id}", response_model=dict)
|
||||||
|
async def mark_project_read(
|
||||||
|
project_id: int,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""将指定项目的所有未读通知标记为已读(打开项目时调用)"""
|
||||||
|
try:
|
||||||
|
count = await notification_service.mark_project_read(current_user.id, project_id)
|
||||||
|
return success_response(data={"marked": count}, message="项目通知已标记为已读")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error marking project read: {e}")
|
||||||
|
return success_response(message="操作完成")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{notification_id}/read", response_model=dict)
|
@router.put("/{notification_id}/read", response_model=dict)
|
||||||
async def mark_as_read(
|
async def mark_as_read(
|
||||||
notification_id: str,
|
notification_id: str,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,40 @@ def get_document_count(storage_key: str) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_document_stats(storage_key: str):
|
||||||
|
"""
|
||||||
|
计算项目文档统计信息:返回 (doc_count, last_activity_at)
|
||||||
|
last_activity 取项目内 .md/.pdf 文档的最新文件修改时间(排除 _assets),
|
||||||
|
反映文档被编辑、导入、Git 拉取等真实活动时间,无需额外数据库字段。
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
try:
|
||||||
|
project_path = storage_service.get_secure_path(storage_key)
|
||||||
|
if not project_path.exists():
|
||||||
|
return 0, None
|
||||||
|
assets_dir = project_path / "_assets"
|
||||||
|
count = 0
|
||||||
|
last_ts = None
|
||||||
|
for p in project_path.rglob("*"):
|
||||||
|
if not p.is_file():
|
||||||
|
continue
|
||||||
|
if p.suffix.lower() not in (".md", ".pdf"):
|
||||||
|
continue
|
||||||
|
if assets_dir in p.parents:
|
||||||
|
continue
|
||||||
|
count += 1
|
||||||
|
try:
|
||||||
|
ts = p.stat().st_mtime
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if last_ts is None or ts > last_ts:
|
||||||
|
last_ts = ts
|
||||||
|
last_activity = datetime.fromtimestamp(last_ts).isoformat() if last_ts else None
|
||||||
|
return count, last_activity
|
||||||
|
except Exception:
|
||||||
|
return 0, None
|
||||||
|
|
||||||
|
|
||||||
async def attach_member_counts(db: AsyncSession, projects) -> dict:
|
async def attach_member_counts(db: AsyncSession, projects) -> dict:
|
||||||
"""批量查询项目的参与人数量(含所有者),返回 {project_id: count}"""
|
"""批量查询项目的参与人数量(含所有者),返回 {project_id: count}"""
|
||||||
if not projects:
|
if not projects:
|
||||||
|
|
@ -96,7 +130,9 @@ async def get_my_projects(
|
||||||
projects_data = []
|
projects_data = []
|
||||||
for p in all_projects:
|
for p in all_projects:
|
||||||
p_dict = ProjectResponse.from_orm(p).dict()
|
p_dict = ProjectResponse.from_orm(p).dict()
|
||||||
p_dict['doc_count'] = get_document_count(p.storage_key)
|
doc_count, last_activity_at = get_project_document_stats(p.storage_key)
|
||||||
|
p_dict['doc_count'] = doc_count
|
||||||
|
p_dict['last_activity_at'] = last_activity_at
|
||||||
p_dict['member_count'] = member_counts.get(p.id, 0)
|
p_dict['member_count'] = member_counts.get(p.id, 0)
|
||||||
projects_data.append(p_dict)
|
projects_data.append(p_dict)
|
||||||
|
|
||||||
|
|
@ -117,7 +153,9 @@ async def get_owned_projects(
|
||||||
projects_data = []
|
projects_data = []
|
||||||
for p in projects:
|
for p in projects:
|
||||||
p_dict = ProjectResponse.from_orm(p).dict()
|
p_dict = ProjectResponse.from_orm(p).dict()
|
||||||
p_dict['doc_count'] = get_document_count(p.storage_key)
|
doc_count, last_activity_at = get_project_document_stats(p.storage_key)
|
||||||
|
p_dict['doc_count'] = doc_count
|
||||||
|
p_dict['last_activity_at'] = last_activity_at
|
||||||
p_dict['member_count'] = member_counts.get(p.id, 0)
|
p_dict['member_count'] = member_counts.get(p.id, 0)
|
||||||
projects_data.append(p_dict)
|
projects_data.append(p_dict)
|
||||||
return success_response(data=projects_data)
|
return success_response(data=projects_data)
|
||||||
|
|
@ -150,7 +188,9 @@ async def get_shared_projects(
|
||||||
project_dict['owner_name'] = owner.username
|
project_dict['owner_name'] = owner.username
|
||||||
project_dict['owner_nickname'] = owner.nickname
|
project_dict['owner_nickname'] = owner.nickname
|
||||||
project_dict['user_role'] = member.role # 添加用户角色
|
project_dict['user_role'] = member.role # 添加用户角色
|
||||||
project_dict['doc_count'] = get_document_count(project.storage_key)
|
doc_count, last_activity_at = get_project_document_stats(project.storage_key)
|
||||||
|
project_dict['doc_count'] = doc_count
|
||||||
|
project_dict['last_activity_at'] = last_activity_at
|
||||||
project_dict['member_count'] = member_counts.get(project.id, 0)
|
project_dict['member_count'] = member_counts.get(project.id, 0)
|
||||||
projects_data.append(project_dict)
|
projects_data.append(project_dict)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
@ -20,6 +21,24 @@ class NotificationService:
|
||||||
def _get_content_key(self, user_id: int) -> str:
|
def _get_content_key(self, user_id: int) -> str:
|
||||||
return f"notifications:content:{user_id}"
|
return f"notifications:content:{user_id}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_project_id_from_link(link: Optional[str]) -> Optional[int]:
|
||||||
|
"""从通知链接中解析项目ID(兼容旧通知没有 project_id 字段的情况)"""
|
||||||
|
if not link:
|
||||||
|
return None
|
||||||
|
match = re.search(r"/projects/(\d+)", link)
|
||||||
|
return int(match.group(1)) if match else None
|
||||||
|
|
||||||
|
def _get_project_id(self, data: Dict[str, Any]) -> Optional[int]:
|
||||||
|
"""从通知数据中获取项目ID"""
|
||||||
|
pid = data.get("project_id")
|
||||||
|
if pid is not None:
|
||||||
|
try:
|
||||||
|
return int(pid)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return self._extract_project_id_from_link(data.get("link"))
|
||||||
|
|
||||||
async def create_notification(
|
async def create_notification(
|
||||||
self,
|
self,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
|
@ -28,7 +47,8 @@ class NotificationService:
|
||||||
content: str = None,
|
content: str = None,
|
||||||
type: str = "info",
|
type: str = "info",
|
||||||
category: str = "system",
|
category: str = "system",
|
||||||
link: str = None
|
link: str = None,
|
||||||
|
project_id: Optional[int] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""创建单条通知 (写入 Redis)"""
|
"""创建单条通知 (写入 Redis)"""
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
|
|
@ -49,6 +69,8 @@ class NotificationService:
|
||||||
"is_read": False,
|
"is_read": False,
|
||||||
"created_at": timestamp
|
"created_at": timestamp
|
||||||
}
|
}
|
||||||
|
if project_id is not None:
|
||||||
|
notification_data["project_id"] = project_id
|
||||||
|
|
||||||
json_data = json.dumps(notification_data, ensure_ascii=False)
|
json_data = json.dumps(notification_data, ensure_ascii=False)
|
||||||
order_key = self._get_order_key(user_id)
|
order_key = self._get_order_key(user_id)
|
||||||
|
|
@ -71,7 +93,9 @@ class NotificationService:
|
||||||
title: str,
|
title: str,
|
||||||
content: str,
|
content: str,
|
||||||
user_ids: List[int],
|
user_ids: List[int],
|
||||||
link: str = None
|
link: str = None,
|
||||||
|
project_id: Optional[int] = None,
|
||||||
|
category: str = "system"
|
||||||
):
|
):
|
||||||
"""向指定多个用户发送系统通知"""
|
"""向指定多个用户发送系统通知"""
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
|
|
@ -89,11 +113,13 @@ class NotificationService:
|
||||||
"title": title,
|
"title": title,
|
||||||
"content": content,
|
"content": content,
|
||||||
"type": "info",
|
"type": "info",
|
||||||
"category": "system",
|
"category": category,
|
||||||
"link": link,
|
"link": link,
|
||||||
"is_read": False,
|
"is_read": False,
|
||||||
"created_at": timestamp
|
"created_at": timestamp
|
||||||
}
|
}
|
||||||
|
if project_id is not None:
|
||||||
|
notification_data["project_id"] = project_id
|
||||||
json_data = json.dumps(notification_data, ensure_ascii=False)
|
json_data = json.dumps(notification_data, ensure_ascii=False)
|
||||||
|
|
||||||
order_key = self._get_order_key(uid)
|
order_key = self._get_order_key(uid)
|
||||||
|
|
@ -130,7 +156,9 @@ class NotificationService:
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
user_ids=member_ids,
|
user_ids=member_ids,
|
||||||
link=link
|
link=link,
|
||||||
|
project_id=project_id,
|
||||||
|
category=category
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_user_notifications(
|
async def get_user_notifications(
|
||||||
|
|
@ -224,6 +252,59 @@ class NotificationService:
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def get_unread_count_by_project(self, user_id: int) -> Dict[int, int]:
|
||||||
|
"""按项目统计未读通知数量(仅项目类通知),返回 {project_id: count}"""
|
||||||
|
redis = get_redis()
|
||||||
|
if not redis:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
content_key = self._get_content_key(user_id)
|
||||||
|
all_jsons = await redis.hvals(content_key)
|
||||||
|
result: Dict[int, int] = {}
|
||||||
|
for js in all_jsons:
|
||||||
|
if not js:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = json.loads(js)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if data.get("is_read"):
|
||||||
|
continue
|
||||||
|
# 关联到项目的未读通知即计入该项目(兼容旧数据 category=system)
|
||||||
|
pid = self._get_project_id(data)
|
||||||
|
if pid:
|
||||||
|
result[pid] = result.get(pid, 0) + 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def mark_project_read(self, user_id: int, project_id: int) -> int:
|
||||||
|
"""将指定项目的未读通知全部标记为已读,返回标记数量"""
|
||||||
|
redis = get_redis()
|
||||||
|
if not redis:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
content_key = self._get_content_key(user_id)
|
||||||
|
all_jsons = await redis.hvals(content_key)
|
||||||
|
updates = {}
|
||||||
|
count = 0
|
||||||
|
for js in all_jsons:
|
||||||
|
if not js:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = json.loads(js)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if data.get("is_read"):
|
||||||
|
continue
|
||||||
|
# 关联到该项目的未读通知全部标记已读(兼容旧数据 category=system)
|
||||||
|
if self._get_project_id(data) == project_id:
|
||||||
|
data["is_read"] = True
|
||||||
|
updates[data.get("id")] = json.dumps(data, ensure_ascii=False)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
await redis.hset(content_key, mapping=updates)
|
||||||
|
return count
|
||||||
|
|
||||||
async def mark_all_read(self, user_id: int):
|
async def mark_all_read(self, user_id: int):
|
||||||
"""标记所有已读"""
|
"""标记所有已读"""
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,23 @@ export function markAllAsRead() {
|
||||||
method: 'put',
|
method: 'put',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按项目统计未读通知数量(与消息通知一致)
|
||||||
|
*/
|
||||||
|
export function getUnreadByProject() {
|
||||||
|
return request({
|
||||||
|
url: '/notifications/unread-by-project',
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定项目的所有未读通知标记为已读(打开项目时调用)
|
||||||
|
*/
|
||||||
|
export function markProjectNotificationsRead(projectId) {
|
||||||
|
return request({
|
||||||
|
url: `/notifications/read-project/${projectId}`,
|
||||||
|
method: 'put',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import { getProjectTree, getFileContent, getDocumentUrl, getExportPdfUrl } from
|
||||||
import { gitPull, gitPush, getGitRepos } from '@/api/project'
|
import { gitPull, gitPush, getGitRepos } from '@/api/project'
|
||||||
import { getFileShareInfo, createOrUpdateFileShare, deleteFileShare } from '@/api/share'
|
import { getFileShareInfo, createOrUpdateFileShare, deleteFileShare } from '@/api/share'
|
||||||
import { searchDocuments } from '@/api/search'
|
import { searchDocuments } from '@/api/search'
|
||||||
|
import { markProjectNotificationsRead } from '@/api/notification'
|
||||||
|
import useNotificationStore from '@/stores/notificationStore'
|
||||||
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
import VirtualPDFViewer from '@/components/PDFViewer/VirtualPDFViewer'
|
||||||
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
||||||
import Toast from '@/components/Toast/Toast'
|
import Toast from '@/components/Toast/Toast'
|
||||||
|
|
@ -196,6 +198,14 @@ function DocumentPage() {
|
||||||
loadFileTree()
|
loadFileTree()
|
||||||
}, [projectId])
|
}, [projectId])
|
||||||
|
|
||||||
|
// 打开项目后,将该项目的未读更新通知标记为已读(与消息通知联动)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) return
|
||||||
|
markProjectNotificationsRead(projectId)
|
||||||
|
.then(() => useNotificationStore.getState().fetchUnreadCount())
|
||||||
|
.catch(() => {})
|
||||||
|
}, [projectId])
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认退出',
|
title: '确认退出',
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,37 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 参与项目右上角:未读更新通知数角标 */
|
||||||
|
.project-card-update-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(135deg, #ff4d4f 0%, #f5222d 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 6px rgba(245, 34, 45, 0.45);
|
||||||
|
animation: project-badge-pop 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes project-badge-pop {
|
||||||
|
from {
|
||||||
|
transform: scale(0.6);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 卡片主体:标题在上、描述占中间弹性空间、元信息贴底 */
|
/* 卡片主体:标题在上、描述占中间弹性空间、元信息贴底 */
|
||||||
.project-card-body {
|
.project-card-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -249,20 +280,18 @@ body.dark .project-card-role-badge.role-editor {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 我的项目卡片:蓝色管理风 ========== */
|
/* ========== 我的项目卡片:蓝色管理风(无顶边) ========== */
|
||||||
.project-card-my {
|
.project-card-my {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-top: 3px solid var(--link-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-my .ant-card-actions {
|
.project-card-my .ant-card-actions {
|
||||||
background: var(--bg-color-secondary);
|
background: var(--bg-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 参与的项目卡片:绿色协作风 ========== */
|
/* ========== 参与的项目卡片:绿色协作风(无顶边) ========== */
|
||||||
.project-card-share {
|
.project-card-share {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-top: 3px solid #52c41a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-share .ant-card-actions {
|
.project-card-share .ant-card-actions {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { getMyProjects, getOwnedProjects, getSharedProjects, createProject, dele
|
||||||
import { getProjectShareInfo, updateProjectShareSettings } from '@/api/share'
|
import { getProjectShareInfo, updateProjectShareSettings } from '@/api/share'
|
||||||
import { getUserList } from '@/api/users'
|
import { getUserList } from '@/api/users'
|
||||||
import { searchDocuments } from '@/api/search'
|
import { searchDocuments } from '@/api/search'
|
||||||
|
import { getUnreadByProject, markProjectNotificationsRead } from '@/api/notification'
|
||||||
|
import useNotificationStore from '@/stores/notificationStore'
|
||||||
import ListActionBar from '@/components/ListActionBar/ListActionBar'
|
import ListActionBar from '@/components/ListActionBar/ListActionBar'
|
||||||
import Toast from '@/components/Toast/Toast'
|
import Toast from '@/components/Toast/Toast'
|
||||||
import useProjectKnowledge from './useProjectKnowledge'
|
import useProjectKnowledge from './useProjectKnowledge'
|
||||||
|
|
@ -39,6 +41,7 @@ function ProjectList({ type = 'my' }) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { user: currentUser } = useUserStore()
|
const { user: currentUser } = useUserStore()
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [unreadByProject, setUnreadByProject] = useState({})
|
||||||
const pageSize = 8
|
const pageSize = 8
|
||||||
|
|
||||||
// 参与项目角色 -> 显示名称
|
// 参与项目角色 -> 显示名称
|
||||||
|
|
@ -59,6 +62,22 @@ function ProjectList({ type = 'my' }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 相对时间(用于“最后更新”)
|
||||||
|
const formatRelativeTime = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
if (Number.isNaN(d.getTime())) return ''
|
||||||
|
const diff = Date.now() - d.getTime()
|
||||||
|
if (diff < 60 * 1000) return '刚刚'
|
||||||
|
const minutes = Math.floor(diff / (60 * 1000))
|
||||||
|
if (minutes < 60) return `${minutes} 分钟前`
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
if (hours < 24) return `${hours} 小时前`
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
if (days < 30) return `${days} 天前`
|
||||||
|
return formatDate(dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
// 卡片归属用户显示名(我的项目 -> 当前用户;参与项目 -> 项目所有者)
|
// 卡片归属用户显示名(我的项目 -> 当前用户;参与项目 -> 项目所有者)
|
||||||
const cardOwnerName = (project) => {
|
const cardOwnerName = (project) => {
|
||||||
if (type === 'my') {
|
if (type === 'my') {
|
||||||
|
|
@ -166,6 +185,14 @@ function ProjectList({ type = 'my' }) {
|
||||||
res = await getMyProjects()
|
res = await getMyProjects()
|
||||||
}
|
}
|
||||||
setProjects(res.data || [])
|
setProjects(res.data || [])
|
||||||
|
// 参与项目:加载每个项目的未读更新通知数(与消息通知保持一致)
|
||||||
|
if (type === 'share') {
|
||||||
|
getUnreadByProject()
|
||||||
|
.then((unreadRes) => setUnreadByProject(unreadRes.data?.unread_by_project || {}))
|
||||||
|
.catch(() => setUnreadByProject({}))
|
||||||
|
} else {
|
||||||
|
setUnreadByProject({})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch projects error:', error)
|
console.error('Fetch projects error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -394,6 +421,20 @@ function ProjectList({ type = 'my' }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenProject = (projectId) => {
|
const handleOpenProject = (projectId) => {
|
||||||
|
// 打开参与项目时,将其未读更新通知标记为已读(与消息通知联动)
|
||||||
|
if (type === 'share' && unreadByProject[projectId]) {
|
||||||
|
markProjectNotificationsRead(projectId)
|
||||||
|
.then(() => {
|
||||||
|
setUnreadByProject((prev) => {
|
||||||
|
if (!prev[projectId]) return prev
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[projectId]
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
useNotificationStore.getState().fetchUnreadCount()
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
navigate(`/projects/${projectId}/docs`)
|
navigate(`/projects/${projectId}/docs`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -685,10 +726,19 @@ function ProjectList({ type = 'my' }) {
|
||||||
<Tooltip key="view" title="进入项目"><EyeOutlined /></Tooltip>,
|
<Tooltip key="view" title="进入项目"><EyeOutlined /></Tooltip>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{/* 公开项目标识 */}
|
{/* 公开项目标识(仅我的项目显示,参与项目不显示) */}
|
||||||
{project.is_public === 1 && (
|
{type === 'my' && project.is_public === 1 && (
|
||||||
<div className="project-card-public-badge">公开</div>
|
<div className="project-card-public-badge">公开</div>
|
||||||
)}
|
)}
|
||||||
|
{/* 参与项目:右上角显示未读更新通知数(打开项目后已读) */}
|
||||||
|
{type === 'share' && (unreadByProject[project.id] || 0) > 0 && (
|
||||||
|
<div
|
||||||
|
className="project-card-update-badge"
|
||||||
|
title={`${unreadByProject[project.id]} 条项目更新通知`}
|
||||||
|
>
|
||||||
|
{unreadByProject[project.id] > 99 ? '99+' : unreadByProject[project.id]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 统一的卡片骨架:标题 + 描述(主要信息)+ 底部统计/归属/角色 */}
|
{/* 统一的卡片骨架:标题 + 描述(主要信息)+ 底部统计/归属/角色 */}
|
||||||
<div className="project-card-body">
|
<div className="project-card-body">
|
||||||
|
|
@ -706,8 +756,8 @@ function ProjectList({ type = 'my' }) {
|
||||||
<span className="project-stat-item" title="文档数量">
|
<span className="project-stat-item" title="文档数量">
|
||||||
<FileTextOutlined /> {project.doc_count || 0} 文档
|
<FileTextOutlined /> {project.doc_count || 0} 文档
|
||||||
</span>
|
</span>
|
||||||
<span className="project-stat-item" title="创建时间">
|
<span className="project-stat-item" title="最后更新时间">
|
||||||
<CalendarOutlined /> {formatDate(project.created_at) || '—'}
|
<CalendarOutlined /> {formatRelativeTime(project.last_activity_at) || formatDate(project.created_at) || '—'}
|
||||||
</span>
|
</span>
|
||||||
<span className="project-stat-item" title="项目参与人数量">
|
<span className="project-stat-item" title="项目参与人数量">
|
||||||
<TeamOutlined /> {project.member_count ?? 0} 人参与
|
<TeamOutlined /> {project.member_count ?? 0} 人参与
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue