diff --git a/backend/app/api/v1/projects.py b/backend/app/api/v1/projects.py
index d6afb82..3fb5c38 100644
--- a/backend/app/api/v1/projects.py
+++ b/backend/app/api/v1/projects.py
@@ -3,7 +3,7 @@
"""
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import delete, select, or_
+from sqlalchemy import delete, select, or_, func
from typing import List
import uuid
import secrets
@@ -54,6 +54,18 @@ def get_document_count(storage_key: str) -> int:
return 0
+async def attach_member_counts(db: AsyncSession, projects) -> dict:
+ """批量查询项目的参与人数量(含所有者),返回 {project_id: count}"""
+ if not projects:
+ return {}
+ result = await db.execute(
+ select(ProjectMember.project_id, func.count(ProjectMember.id))
+ .where(ProjectMember.project_id.in_([p.id for p in projects]))
+ .group_by(ProjectMember.project_id)
+ )
+ return dict(result.all())
+
+
@router.get("/", response_model=dict)
async def get_my_projects(
current_user: User = Depends(get_current_user),
@@ -80,10 +92,12 @@ async def get_my_projects(
# 合并结果
all_projects = owned_projects + member_projects
+ member_counts = await attach_member_counts(db, all_projects)
projects_data = []
for p in all_projects:
p_dict = ProjectResponse.from_orm(p).dict()
p_dict['doc_count'] = get_document_count(p.storage_key)
+ p_dict['member_count'] = member_counts.get(p.id, 0)
projects_data.append(p_dict)
return success_response(data=projects_data)
@@ -99,10 +113,12 @@ async def get_owned_projects(
select(Project).where(Project.owner_id == current_user.id, Project.status == 1)
)
projects = result.scalars().all()
+ member_counts = await attach_member_counts(db, projects)
projects_data = []
for p in projects:
p_dict = ProjectResponse.from_orm(p).dict()
p_dict['doc_count'] = get_document_count(p.storage_key)
+ p_dict['member_count'] = member_counts.get(p.id, 0)
projects_data.append(p_dict)
return success_response(data=projects_data)
@@ -125,6 +141,9 @@ async def get_shared_projects(
)
projects_with_info = result.all()
+ projects = [project for project, _, _ in projects_with_info]
+ member_counts = await attach_member_counts(db, projects)
+
projects_data = []
for project, owner, member in projects_with_info:
project_dict = ProjectResponse.from_orm(project).dict()
@@ -132,6 +151,7 @@ async def get_shared_projects(
project_dict['owner_nickname'] = owner.nickname
project_dict['user_role'] = member.role # 添加用户角色
project_dict['doc_count'] = get_document_count(project.storage_key)
+ project_dict['member_count'] = member_counts.get(project.id, 0)
projects_data.append(project_dict)
return success_response(data=projects_data)
diff --git a/frontend/src/index.css b/frontend/src/index.css
index f380e4a..9cf4a4e 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -19,6 +19,7 @@
--toolbar-bg: #fafafa;
--link-color: #1677ff;
--panel-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
+ --selected-bg: rgba(22, 119, 255, 0.08);
}
body.dark {
@@ -42,6 +43,7 @@ body.dark {
--toolbar-bg: #1f1f1f;
--link-color: #177ddc;
--panel-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
+ --selected-bg: rgba(23, 125, 220, 0.18);
}
body {
diff --git a/frontend/src/pages/Document/DocumentEditor.css b/frontend/src/pages/Document/DocumentEditor.css
index 54c9a86..6b3946e 100644
--- a/frontend/src/pages/Document/DocumentEditor.css
+++ b/frontend/src/pages/Document/DocumentEditor.css
@@ -166,14 +166,36 @@
white-space: nowrap;
}
-/* 选中的文件夹样式 */
+/* ===== 选中节点:文字、图标与背景同步高亮 ===== */
+
+/* 选中叶子节点(文件)的整行背景 */
+.file-tree .ant-menu-item-selected {
+ background-color: var(--selected-bg) !important;
+}
+
+/* 选中文件夹标题的背景 */
.file-tree .folder-selected>.ant-menu-submenu-title {
- background-color: var(--item-hover-bg) !important;
+ background-color: var(--selected-bg) !important;
color: var(--link-color) !important;
}
.file-tree .folder-selected>.ant-menu-submenu-title:hover {
- background-color: var(--item-hover-bg) !important;
+ background-color: var(--selected-bg) !important;
+}
+
+/* 选中时文字颜色与字重(修复仅图标高亮、文字不变的问题) */
+.file-tree .ant-menu-item-selected .tree-node-wrapper,
+.file-tree .folder-selected>.ant-menu-submenu-title .tree-node-wrapper,
+.file-tree .ant-menu-submenu-selected>.ant-menu-submenu-title .tree-node-wrapper {
+ color: var(--link-color) !important;
+ font-weight: 500;
+}
+
+/* 选中时图标颜色(文件夹图标不再依赖内联 style,统一走主题色) */
+.file-tree .ant-menu-item-selected .ant-menu-item-icon,
+.file-tree .folder-selected>.ant-menu-submenu-title .ant-menu-item-icon,
+.file-tree .ant-menu-submenu-selected>.ant-menu-submenu-title .ant-menu-item-icon {
+ color: var(--link-color) !important;
}
.file-tree .ant-menu-item,
diff --git a/frontend/src/pages/Document/DocumentEditor.jsx b/frontend/src/pages/Document/DocumentEditor.jsx
index 73255fb..83ae353 100644
--- a/frontend/src/pages/Document/DocumentEditor.jsx
+++ b/frontend/src/pages/Document/DocumentEditor.jsx
@@ -1019,14 +1019,11 @@ function DocumentEditor() {
if (!node.isLeaf) {
const isOpen = openKeys.includes(node.key)
- const folderIconStyle = isSelected ? { color: '#1890ff' } : undefined
- // 目录 - 通过className和style控制选中样式
+ // 目录 - 选中样式(背景/文字/图标)统一由 CSS 类控制,保证文字与图标同步高亮
return {
key: node.key,
label: labelContent,
- icon: isOpen
- ?
- : ,
+ icon: isOpen ? : ,
children: node.children ? convertTreeToMenuItems(node.children) : [],
className: isSelected ? 'folder-selected' : '',
onTitleClick: () => {
diff --git a/frontend/src/pages/Document/DocumentPage.css b/frontend/src/pages/Document/DocumentPage.css
index f1ae619..093586b 100644
--- a/frontend/src/pages/Document/DocumentPage.css
+++ b/frontend/src/pages/Document/DocumentPage.css
@@ -280,6 +280,121 @@
font-size: 14px;
}
+/* ===== 文件夹内容视图 ===== */
+.docs-folder-view {
+ padding: 4px 0 24px;
+}
+
+.docs-folder-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 14px;
+ margin-bottom: 16px;
+ background: var(--bg-color-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 10px;
+ color: var(--text-color-secondary);
+ font-size: 13px;
+ min-width: 0;
+}
+
+.docs-folder-path {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.docs-folder-path-icon {
+ flex: none;
+ color: #faad14;
+ font-size: 15px;
+}
+
+.docs-folder-path-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 500;
+ color: var(--text-color);
+}
+
+.docs-folder-counts {
+ flex: none;
+ white-space: nowrap;
+}
+
+.docs-folder-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+ gap: 14px;
+}
+
+.docs-folder-item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 16px 18px;
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ background: var(--card-bg);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 0;
+}
+
+.docs-folder-item:hover {
+ border-color: var(--link-color);
+ box-shadow: var(--panel-shadow);
+ transform: translateY(-2px);
+}
+
+.docs-folder-item:active {
+ transform: translateY(0);
+}
+
+.docs-folder-item-icon {
+ font-size: 28px;
+ line-height: 1;
+ color: #faad14;
+}
+
+.docs-folder-item-icon.md {
+ color: var(--link-color);
+}
+
+.docs-folder-item-icon.pdf {
+ color: #f5222d;
+}
+
+.docs-folder-item-name {
+ width: 100%;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-color);
+ min-width: 0;
+}
+
+.docs-folder-item-share {
+ flex: none;
+ color: var(--link-color);
+ font-size: 12px;
+}
+
+.docs-folder-item-count {
+ font-size: 12px;
+ color: var(--text-color-secondary);
+}
+
.markdown-body {
font-size: 16px;
line-height: 1.6;
diff --git a/frontend/src/pages/Document/DocumentPage.jsx b/frontend/src/pages/Document/DocumentPage.jsx
index eac1255..2e18d5a 100644
--- a/frontend/src/pages/Document/DocumentPage.jsx
+++ b/frontend/src/pages/Document/DocumentPage.jsx
@@ -128,7 +128,7 @@ function DocumentPage() {
}
}
- const selectFolder = (folderPath, { syncUrl = false } = {}) => {
+ const selectFolder = (folderPath, { syncUrl = false, expandSelf = false } = {}) => {
setSelectedFile('')
setSelectedNodeKey(folderPath)
setMarkdownContent('')
@@ -136,11 +136,28 @@ function DocumentPage() {
setViewMode('folder')
expandParentFolders(`${folderPath}/placeholder`)
+ // 从内容区点击进入子文件夹时,同时展开自身,保证与左侧文件树同步
+ if (expandSelf) {
+ setOpenKeys(prev => (prev.includes(folderPath) ? prev : [...prev, folderPath]))
+ }
+
if (syncUrl) {
updateSelectedParam(folderPath, false)
}
}
+ // 获取文件夹内的子节点(与左侧文件树保持一致:文件夹 + md/pdf 文档)
+ const getFolderChildren = (folderPath) => {
+ const node = folderPath ? findNodeByKey(fileTree, folderPath) : null
+ if (!node || node.isLeaf) return { folders: [], files: [], node }
+ const children = node.children || []
+ const folders = children.filter(c => !c.isLeaf)
+ const files = children.filter(c =>
+ c.isLeaf && (c.title.toLowerCase().endsWith('.md') || c.title.toLowerCase().endsWith('.pdf'))
+ )
+ return { folders, files, node }
+ }
+
const openDocumentPath = (filePath, { syncUrl = false } = {}) => {
setSelectedFile(filePath)
setSelectedNodeKey(filePath)
@@ -1257,10 +1274,75 @@ function DocumentPage() {
toolbarTarget={pdfToolbarTarget}
/>
) : viewMode === 'folder' ? (
-
-
- 已选择文件夹,请从左侧选择 Markdown 或 PDF 文件查看。
-
+ (() => {
+ const { folders, files, node } = getFolderChildren(selectedNodeKey)
+ if (!node || node.isLeaf) {
+ return (
+
+
+ 已选择文件夹,请从左侧选择 Markdown 或 PDF 文件查看。
+
+ )
+ }
+ const total = folders.length + files.length
+ return (
+
+
+
+
+ {node.key || '根目录'}
+
+
+ {folders.length} 个文件夹 · {files.length} 个文档
+
+
+
+ {total === 0 ? (
+
+ ) : (
+
+ {folders.map(sub => (
+
selectFolder(sub.key, { syncUrl: true, expandSelf: true })}
+ title={sub.title}
+ >
+
+ {sub.title}
+ {sub.children?.length || 0} 项
+
+ ))}
+ {files.map(file => {
+ const isPdf = file.title.toLowerCase().endsWith('.pdf')
+ const displayName = isPdf ? file.title : file.title.replace(/.md$/i, '')
+ return (
+
openDocumentPath(file.key, { syncUrl: true })}
+ title={file.title}
+ >
+ {isPdf
+ ?
+ : }
+
+ {displayName}
+ {file.is_shared && }
+
+ {isPdf ? 'PDF 文档' : 'Markdown 文档'}
+
+ )
+ })}
+
+ )}
+
+ )
+ })()
) : isLargeMarkdown ? (
* {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.project-stat-item {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ white-space: nowrap;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.project-card-owner {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ min-width: 0;
+ max-width: 100%;
+}
+
+.project-card-owner-avatar {
+ flex: none;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #1677ff 0%, #69b1ff 100%);
+ color: #fff;
+ font-size: 11px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+}
+
+.project-card-share .project-card-owner-avatar {
+ background: linear-gradient(135deg, #52c41a 0%, #95de64 100%);
+}
+
+.project-card-owner-name {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-color);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100%;
+}
+
+/* 项目角色徽章(重新设计:药丸式彩色徽章 + 角色图标) */
+.project-card-role-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 2px 9px;
+ border-radius: 999px;
+ font-size: 11px;
+ line-height: 16px;
+ font-weight: 500;
+ white-space: nowrap;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* 所有者 / 管理者:金色徽章 */
+.project-card-role-badge.role-owner,
+.project-card-role-badge.role-admin {
+ background: rgba(250, 173, 20, 0.16);
+ color: #d48806;
+}
+
+body.dark .project-card-role-badge.role-owner,
+body.dark .project-card-role-badge.role-admin {
+ background: rgba(250, 173, 20, 0.22);
+ color: #ffc53d;
+}
+
+/* 编辑者:蓝色徽章 */
+.project-card-role-badge.role-editor {
+ background: rgba(22, 119, 255, 0.12);
+ color: var(--link-color);
+}
+
+body.dark .project-card-role-badge.role-editor {
+ background: rgba(23, 125, 220, 0.25);
+ color: #4c9be8;
+}
+
+/* 查看者:灰色徽章 */
+.project-card-role-badge.role-viewer {
+ background: var(--item-hover-bg);
+ color: var(--text-color-secondary);
+}
+
+/* ========== 我的项目卡片:蓝色管理风 ========== */
+.project-card-my {
+ text-align: left;
+ border-top: 3px solid var(--link-color);
+}
+
+.project-card-my .ant-card-actions {
+ background: var(--bg-color-secondary);
+}
+
+/* ========== 参与的项目卡片:绿色协作风 ========== */
+.project-card-share {
+ text-align: left;
+ border-top: 3px solid #52c41a;
+}
+
+.project-card-share .ant-card-actions {
+ background: rgba(82, 196, 26, 0.05);
+}
+
+.project-card-share .ant-card-actions li span {
+ color: #52c41a;
+}
+
+/* ========== 搜索结果卡片(保持旧版居中风格) ========== */
+.project-card-icon {
+ margin-bottom: 16px;
+ text-align: center;
}
/* 知识库向量化弹窗:未配置向量模型时的提示条 */
@@ -106,19 +305,28 @@ body.dark .kb-warning-banner {
margin-top: 8px;
}
-/* 圆点分页指示器样式 */
+/* ========== 圆点分页指示器(对称布局) ========== */
.dot-pagination.ant-pagination {
display: flex;
align-items: center;
+ justify-content: center;
+ gap: 2px;
+ flex-wrap: nowrap;
}
-.dot-pagination .ant-pagination-item {
+.dot-pagination .ant-pagination-item,
+.dot-pagination .ant-pagination-prev,
+.dot-pagination .ant-pagination-next {
border: none !important;
background: transparent !important;
- min-width: 16px !important;
- height: 16px !important;
- line-height: 16px !important;
- margin: 0 4px !important;
+ margin: 0 !important;
+ min-width: 24px !important;
+ height: 24px !important;
+ line-height: 24px !important;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ vertical-align: middle;
}
.dot-pagination .ant-pagination-item a {
@@ -132,19 +340,67 @@ body.dark .kb-warning-banner {
border-radius: 50%;
background-color: var(--text-color-secondary);
opacity: 0.3;
- margin: 4px auto;
transition: all 0.3s;
}
.dot-pagination .ant-pagination-item-active .pagination-dot {
background-color: var(--link-color);
opacity: 1;
- transform: scale(1.2);
+ transform: scale(1.25);
}
-.dot-pagination .ant-pagination-prev,
-.dot-pagination .ant-pagination-next {
+.dot-pagination .ant-pagination-jump-prev,
+.dot-pagination .ant-pagination-jump-next {
+ border: none !important;
+ background: transparent !important;
+ margin: 0 !important;
min-width: 24px !important;
height: 24px !important;
line-height: 24px !important;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-color-secondary);
+ font-size: 12px;
+}
+
+.dot-pagination .ant-pagination-jump-prev .ant-pagination-item-container,
+.dot-pagination .ant-pagination-jump-next .ant-pagination-item-container {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.dot-pagination .ant-pagination-prev .ant-pagination-item-link,
+.dot-pagination .ant-pagination-next .ant-pagination-item-link {
+ border: none !important;
+ background: transparent !important;
+ color: var(--text-color-secondary);
+ font-size: 12px;
+ line-height: 1;
+ height: 100%;
+ width: 100%;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ padding: 0;
+ transition: all 0.2s;
+}
+
+.dot-pagination .ant-pagination-prev:hover .ant-pagination-item-link,
+.dot-pagination .ant-pagination-next:hover .ant-pagination-item-link {
+ color: var(--link-color);
+ background: var(--item-hover-bg) !important;
+}
+
+.dot-pagination .ant-pagination-prev:active .ant-pagination-item-link,
+.dot-pagination .ant-pagination-next:active .ant-pagination-item-link {
+ background: var(--item-hover-bg) !important;
+}
+
+.dot-pagination .ant-pagination-disabled .ant-pagination-item-link {
+ color: var(--text-color-secondary) !important;
+ opacity: 0.35;
+ cursor: not-allowed;
}
diff --git a/frontend/src/pages/ProjectList/ProjectList.jsx b/frontend/src/pages/ProjectList/ProjectList.jsx
index e9d1ec7..13ded7e 100644
--- a/frontend/src/pages/ProjectList/ProjectList.jsx
+++ b/frontend/src/pages/ProjectList/ProjectList.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Empty, Modal, Form, Input, Row, Col, Space, Button, Switch, message, Select, Table, Tag, Pagination, Progress, Alert, List, Spin, Tooltip } from 'antd'
-import { PlusOutlined, FolderOutlined, TeamOutlined, EyeOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileOutlined, GithubOutlined, CheckOutlined, SwapOutlined, SettingOutlined, DatabaseOutlined, ReloadOutlined } from '@ant-design/icons'
+import { PlusOutlined, FolderOutlined, TeamOutlined, EyeOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileOutlined, GithubOutlined, CheckOutlined, SwapOutlined, SettingOutlined, DatabaseOutlined, ReloadOutlined, CalendarOutlined, FileTextOutlined, CrownOutlined } from '@ant-design/icons'
import { getMyProjects, getOwnedProjects, getSharedProjects, createProject, deleteProject, updateProject, getProjectMembers, addProjectMember, removeProjectMember, getGitRepos, createGitRepo, updateGitRepo, deleteGitRepo, transferProject } from '@/api/project'
import { getProjectShareInfo, updateProjectShareSettings } from '@/api/share'
import { getUserList } from '@/api/users'
@@ -9,6 +9,7 @@ import { searchDocuments } from '@/api/search'
import ListActionBar from '@/components/ListActionBar/ListActionBar'
import Toast from '@/components/Toast/Toast'
import useProjectKnowledge from './useProjectKnowledge'
+import useUserStore from '@/stores/userStore'
import './ProjectList.css'
function ProjectList({ type = 'my' }) {
@@ -36,9 +37,62 @@ function ProjectList({ type = 'my' }) {
const [transferModalVisible, setTransferModalVisible] = useState(false)
const [transferForm] = Form.useForm()
const navigate = useNavigate()
+ const { user: currentUser } = useUserStore()
const [currentPage, setCurrentPage] = useState(1)
const pageSize = 8
+ // 参与项目角色 -> 显示名称
+ const projectRoleLabel = (role) => {
+ const roleMap = { admin: '管理者', editor: '编辑者', viewer: '查看者' }
+ return roleMap[role] || role || '查看者'
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr) => {
+ if (!dateStr) return ''
+ try {
+ const d = new Date(dateStr)
+ if (Number.isNaN(d.getTime())) return ''
+ return d.toLocaleDateString('zh-CN')
+ } catch {
+ return ''
+ }
+ }
+
+ // 卡片归属用户显示名(我的项目 -> 当前用户;参与项目 -> 项目所有者)
+ const cardOwnerName = (project) => {
+ if (type === 'my') {
+ return currentUser?.nickname || currentUser?.username || '我'
+ }
+ return project.owner_nickname || project.owner_name || '未知'
+ }
+
+ // 卡片归属用户头像首字符
+ const cardOwnerInitial = (project) => {
+ const name = cardOwnerName(project)
+ return (name || '?').charAt(0).toUpperCase()
+ }
+
+ // 卡片角色显示名(我的项目 -> 所有者;参与项目 -> 我的角色)
+ const cardRoleLabel = (project) => {
+ if (type === 'my') return '所有者'
+ return projectRoleLabel(project.user_role)
+ }
+
+ // 卡片角色徽章样式类
+ const cardRoleClass = (project) => {
+ if (type === 'my') return 'role-owner'
+ const roleMap = { admin: 'role-admin', editor: 'role-editor', viewer: 'role-viewer' }
+ return roleMap[project.user_role] || 'role-viewer'
+ }
+
+ // 卡片角色徽章图标
+ const cardRoleIcon = (project) => {
+ if (type === 'my' || project.user_role === 'admin') return
+ if (project.user_role === 'editor') return
+ return
+ }
+
const {
kbModalVisible,
progress,
@@ -620,7 +674,7 @@ function ProjectList({ type = 'my' }) {
handleOpenProject(project.id)}
actions={type === 'my' ? [
handleEdit(e, project)} />,
@@ -628,30 +682,53 @@ function ProjectList({ type = 'my' }) {
handleKnowledge(e, project)} />,
handleMembers(e, project)} />,
] : [
- ,
+ ,
]}
>
{/* 公开项目标识 */}
{project.is_public === 1 && (
公开
)}
-
-
-
- {project.name}
- {project.description || '暂无描述'}
-
-
文档数: {project.doc_count || 0}
- {type === 'share' && project.owner_name && (
-
- 所有者: {project.owner_nickname || project.owner_name}
+
+ {/* 统一的卡片骨架:标题 + 描述(主要信息)+ 底部统计/归属/角色 */}
+
+
+
+ {type === 'my' ? : }
- )}
- {type === 'share' && project.user_role && (
-
- 角色: {project.user_role === 'admin' ? '管理者' : project.user_role === 'editor' ? '编辑者' : '查看者'}
-
- )}
+
{project.name}
+
+
{project.description || '暂无描述'}
+ {/* 归属人信息 + 项目信息:单行展示 */}
+
+ {type === 'my' ? (
+ <>
+
+ {project.doc_count || 0} 文档
+
+
+ {formatDate(project.created_at) || '—'}
+
+
+ {project.member_count ?? 0} 人参与
+
+ >
+ ) : (
+ <>
+
+ {project.doc_count || 0} 文档
+
+
+ {cardOwnerInitial(project)}
+ {cardOwnerName(project)}
+
+
+ {cardRoleIcon(project)}
+ {cardRoleLabel(project)}
+
+ >
+ )}
+