{item.content}
diff --git a/frontend/src/pages/Desktop.jsx b/frontend/src/pages/Desktop.jsx
index 11aed7a..0afd05b 100644
--- a/frontend/src/pages/Desktop.jsx
+++ b/frontend/src/pages/Desktop.jsx
@@ -98,7 +98,7 @@ function Desktop() {
}
// 跳转到文档页面
- navigate(`/projects/${log.project_id}/documents?path=${encodeURIComponent(log.file_path)}`)
+ navigate(`/projects/${log.project_id}/docs?file=${encodeURIComponent(log.file_path)}`)
}
return (
diff --git a/frontend/src/pages/Document/DocumentPage.jsx b/frontend/src/pages/Document/DocumentPage.jsx
index f353826..6d7ffe1 100644
--- a/frontend/src/pages/Document/DocumentPage.jsx
+++ b/frontend/src/pages/Document/DocumentPage.jsx
@@ -67,11 +67,62 @@ function DocumentPage() {
loadFileTree()
}, [projectId])
+ // 监听 URL 参数变化,处理文件导航和搜索
useEffect(() => {
- if (userRole && userRole !== 'viewer') {
- loadGitRepos()
+ // 只有当文件树加载完成后才处理导航,否则无法正确展开目录
+ if (fileTree.length === 0) return
+
+ const fileParam = searchParams.get('file')
+ const keywordParam = searchParams.get('keyword')
+
+ // 处理搜索
+ if (keywordParam && keywordParam !== searchKeyword) {
+ handleSearch(keywordParam)
}
- }, [projectId, userRole])
+
+ // 处理文件加载
+ if (fileParam) {
+ if (fileParam !== selectedFile) {
+ setSelectedFile(fileParam)
+
+ // 展开父目录
+ const parts = fileParam.split('/')
+ const allParentPaths = []
+ let currentPath = ''
+ for (let i = 0; i < parts.length - 1; i++) {
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
+ allParentPaths.push(currentPath)
+ }
+ if (allParentPaths.length > 0) {
+ setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
+ }
+
+ // 处理 PDF 或 Markdown
+ if (fileParam.toLowerCase().endsWith('.pdf')) {
+ let url = getDocumentUrl(projectId, fileParam)
+ const token = localStorage.getItem('access_token')
+ if (token) {
+ url += `?token=${encodeURIComponent(token)}`
+ }
+ setPdfUrl(url)
+ setPdfFilename(fileParam.split('/').pop())
+ setViewMode('pdf')
+ } else {
+ loadMarkdown(fileParam)
+ setViewMode('markdown')
+ }
+ }
+ } else {
+ // 如果没有指定文件,且当前没有选中文件,默认打开 README.md
+ if (!selectedFile) {
+ const readmeNode = findReadme(fileTree)
+ if (readmeNode) {
+ setSelectedFile(readmeNode.key)
+ loadMarkdown(readmeNode.key)
+ }
+ }
+ }
+ }, [searchParams, fileTree])
// 处理搜索
const handleSearch = async (value) => {
@@ -152,52 +203,6 @@ function DocumentPage() {
setFileTree(tree)
setUserRole(role)
setProjectName(name)
-
- // 检查 URL 是否指定了文件
- const fileParam = searchParams.get('file')
- const keywordParam = searchParams.get('keyword')
-
- if (keywordParam) {
- handleSearch(keywordParam)
- }
-
- if (fileParam) {
- setSelectedFile(fileParam)
-
- // 展开父目录
- const parts = fileParam.split('/')
- const allParentPaths = []
- let currentPath = ''
- for (let i = 0; i < parts.length - 1; i++) {
- const part = parts[i]
- currentPath = currentPath ? `${currentPath}/${part}` : part
- allParentPaths.push(currentPath)
- }
- if (allParentPaths.length > 0) {
- setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
- }
-
- // 处理 PDF 或 Markdown
- if (fileParam.toLowerCase().endsWith('.pdf')) {
- let url = getDocumentUrl(projectId, fileParam)
- const token = localStorage.getItem('access_token')
- if (token) {
- url += `?token=${encodeURIComponent(token)}`
- }
- setPdfUrl(url)
- setPdfFilename(fileParam.split('/').pop())
- setViewMode('pdf')
- } else {
- loadMarkdown(fileParam)
- }
- } else {
- // 默认打开 README.md
- const readmeNode = findReadme(tree)
- if (readmeNode) {
- setSelectedFile(readmeNode.key)
- loadMarkdown(readmeNode.key)
- }
- }
} catch (error) {
console.error('Load file tree error:', error)
}
diff --git a/frontend/src/pages/Notifications/NotificationList.jsx b/frontend/src/pages/Notifications/NotificationList.jsx
index 03cadde..7282c53 100644
--- a/frontend/src/pages/Notifications/NotificationList.jsx
+++ b/frontend/src/pages/Notifications/NotificationList.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
-import { Layout, List, Avatar, Button, Tabs, Space, Tag, Typography, Empty, Pagination } from 'antd'
+import { Layout, List, Avatar, Button, Tabs, Space, Tag, Typography, Empty, Pagination, Badge } from 'antd'
import {
BellOutlined,
CheckOutlined,
@@ -9,6 +9,7 @@ import {
} from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { getNotifications, markAsRead, markAllAsRead } from '@/api/notification'
+import useNotificationStore from '@/stores/notificationStore'
import Toast from '@/components/Toast/Toast'
import PageHeader from '@/components/PageHeader/PageHeader'
@@ -23,6 +24,7 @@ function NotificationList() {
const [pageSize] = useState(10)
const [activeTab, setActiveTab] = useState('all')
const navigate = useNavigate()
+ const { decrementUnreadCount, resetUnreadCount } = useNotificationStore()
useEffect(() => {
fetchNotifications()
@@ -49,7 +51,8 @@ function NotificationList() {
const handleMarkRead = async (id) => {
try {
await markAsRead(id)
- setNotifications(notifications.map(n => n.id === id ? { ...n, is_read: 1 } : n))
+ setNotifications(notifications.map(n => n.id === id ? { ...n, is_read: true } : n))
+ decrementUnreadCount()
} catch (error) {
console.error('Mark read error:', error)
}
@@ -58,6 +61,7 @@ function NotificationList() {
const handleMarkAllRead = async () => {
try {
await markAllAsRead()
+ resetUnreadCount()
Toast.success('操作成功', '所有消息已标记为已读')
fetchNotifications()
} catch (error) {
@@ -66,7 +70,7 @@ function NotificationList() {
}
const handleNotificationClick = (n) => {
- if (n.is_read === 0) {
+ if (!n.is_read) {
handleMarkRead(n.id)
}
if (n.link) {
@@ -82,10 +86,12 @@ function NotificationList() {
}
}
+ const { unreadCount } = useNotificationStore()
+
return (
}
extra={
} onClick={handleMarkAllRead}>
@@ -99,8 +105,24 @@ function NotificationList() {
activeKey={activeTab}
onChange={(key) => { setActiveTab(key); setPage(1); }}
items={[
- { key: 'all', label: '全部消息' },
- { key: 'unread', label: '未读消息' },
+ {
+ key: 'all',
+ label: (
+
+ 全部消息
+ {activeTab === 'all' && }
+
+ )
+ },
+ {
+ key: 'unread',
+ label: (
+
+ 未读消息
+
+
+ )
+ },
]}
/>
@@ -112,12 +134,12 @@ function NotificationList() {
renderItem={(item) => (
handleNotificationClick(item)}
@@ -126,7 +148,7 @@ function NotificationList() {
-
+
{item.title}
@@ -136,7 +158,7 @@ function NotificationList() {
{item.content}
- {item.is_read === 0 && 未读}
+ {!item.is_read && 未读}
diff --git a/frontend/src/pages/Preview/PreviewPage.jsx b/frontend/src/pages/Preview/PreviewPage.jsx
index c20149a..f173782 100644
--- a/frontend/src/pages/Preview/PreviewPage.jsx
+++ b/frontend/src/pages/Preview/PreviewPage.jsx
@@ -106,6 +106,59 @@ function PreviewPage() {
loadProjectInfo()
}, [projectId])
+ // 监听 URL 参数变化,处理文件导航和搜索
+ useEffect(() => {
+ if (fileTree.length === 0) return
+
+ const fileParam = searchParams.get('file')
+ const keywordParam = searchParams.get('keyword')
+
+ if (keywordParam && keywordParam !== searchKeyword) {
+ handleSearch(keywordParam)
+ }
+
+ if (fileParam) {
+ if (fileParam !== selectedFile) {
+ // Deep link to file
+ if (fileParam.toLowerCase().endsWith('.pdf')) {
+ let url = getPreviewDocumentUrl(projectId, fileParam)
+ const params = []
+ if (accessPassword) params.push(`access_pass=${encodeURIComponent(accessPassword)}`)
+ const token = localStorage.getItem('access_token')
+ if (token) params.push(`token=${encodeURIComponent(token)}`)
+ if (params.length > 0) url += `?${params.join('&')}`
+
+ setSelectedFile(fileParam)
+ setPdfUrl(url)
+ setPdfFilename(fileParam.split('/').pop())
+ setViewMode('pdf')
+ } else {
+ setSelectedFile(fileParam)
+ loadMarkdown(fileParam, accessPassword)
+ setViewMode('markdown')
+ }
+
+ // Expand tree to file
+ const parts = fileParam.split('/')
+ const allParentPaths = []
+ let currentPath = ''
+ for (let i = 0; i < parts.length - 1; i++) {
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
+ allParentPaths.push(currentPath)
+ }
+ setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
+ }
+ } else {
+ if (!selectedFile) {
+ const readmeNode = findReadme(fileTree)
+ if (readmeNode) {
+ setSelectedFile(readmeNode.key)
+ loadMarkdown(readmeNode.key, accessPassword)
+ }
+ }
+ }
+ }, [searchParams, fileTree, accessPassword])
+
// 加载项目基本信息
const loadProjectInfo = async () => {
try {
@@ -148,56 +201,6 @@ function PreviewPage() {
const res = await getPreviewTree(projectId, pwd || accessPassword)
const tree = res.data || []
setFileTree(tree)
-
- const readmeNode = findReadme(tree)
-
- // Check query params
- const fileParam = searchParams.get('file')
- const keywordParam = searchParams.get('keyword')
-
- if (keywordParam) {
- handleSearch(keywordParam)
- }
-
- if (fileParam) {
- // Deep link to file
- if (fileParam.toLowerCase().endsWith('.pdf')) {
- let url = getPreviewDocumentUrl(projectId, fileParam)
- // ... params logic repeated from handleMenuClick ...
- // Simplify: just call logic or set state
- // Since we need token/password logic, let's reuse handleMenuClick logic if possible or copy it.
- // For simplicity, just set selection and let user click? No, auto load.
-
- // Copy logic for PDF url construction
- const params = []
- if (pwd || accessPassword) params.push(`access_pass=${encodeURIComponent(pwd || accessPassword)}`)
- const token = localStorage.getItem('access_token')
- if (token) params.push(`token=${encodeURIComponent(token)}`)
- if (params.length > 0) url += `?${params.join('&')}`
-
- setSelectedFile(fileParam)
- setPdfUrl(url)
- setPdfFilename(fileParam.split('/').pop())
- setViewMode('pdf')
- } else {
- setSelectedFile(fileParam)
- loadMarkdown(fileParam, pwd || accessPassword)
- }
-
- // Expand tree to file
- const parts = fileParam.split('/')
- const allParentPaths = []
- let currentPath = ''
- for (let i = 0; i < parts.length - 1; i++) {
- currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]
- allParentPaths.push(currentPath)
- }
- setOpenKeys(prev => [...new Set([...prev, ...allParentPaths])])
-
- } else if (readmeNode) {
- setSelectedFile(readmeNode.key)
- loadMarkdown(readmeNode.key, pwd || accessPassword)
- }
} catch (error) {
console.error('Load file tree error:', error)
if (error.response?.status === 403) {
diff --git a/frontend/src/stores/notificationStore.js b/frontend/src/stores/notificationStore.js
new file mode 100644
index 0000000..ee6e4a3
--- /dev/null
+++ b/frontend/src/stores/notificationStore.js
@@ -0,0 +1,19 @@
+import { create } from 'zustand'
+import { getUnreadCount } from '@/api/notification'
+
+const useNotificationStore = create((set, get) => ({
+ unreadCount: 0,
+ setUnreadCount: (count) => set({ unreadCount: count }),
+ decrementUnreadCount: () => set((state) => ({ unreadCount: Math.max(0, state.unreadCount - 1) })),
+ fetchUnreadCount: async () => {
+ try {
+ const res = await getUnreadCount()
+ set({ unreadCount: res.data.unread_count })
+ } catch (error) {
+ console.error('Fetch unread count error:', error)
+ }
+ },
+ resetUnreadCount: () => set({ unreadCount: 0 }),
+}))
+
+export default useNotificationStore