优化界面
parent
416ef48395
commit
2dbff78698
|
|
@ -3,7 +3,7 @@
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import delete, select, or_
|
from sqlalchemy import delete, select, or_, func
|
||||||
from typing import List
|
from typing import List
|
||||||
import uuid
|
import uuid
|
||||||
import secrets
|
import secrets
|
||||||
|
|
@ -54,6 +54,18 @@ def get_document_count(storage_key: str) -> int:
|
||||||
return 0
|
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)
|
@router.get("/", response_model=dict)
|
||||||
async def get_my_projects(
|
async def get_my_projects(
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
|
|
@ -80,10 +92,12 @@ async def get_my_projects(
|
||||||
|
|
||||||
# 合并结果
|
# 合并结果
|
||||||
all_projects = owned_projects + member_projects
|
all_projects = owned_projects + member_projects
|
||||||
|
member_counts = await attach_member_counts(db, all_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)
|
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)
|
projects_data.append(p_dict)
|
||||||
|
|
||||||
return success_response(data=projects_data)
|
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)
|
select(Project).where(Project.owner_id == current_user.id, Project.status == 1)
|
||||||
)
|
)
|
||||||
projects = result.scalars().all()
|
projects = result.scalars().all()
|
||||||
|
member_counts = await attach_member_counts(db, 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)
|
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)
|
projects_data.append(p_dict)
|
||||||
return success_response(data=projects_data)
|
return success_response(data=projects_data)
|
||||||
|
|
||||||
|
|
@ -125,6 +141,9 @@ async def get_shared_projects(
|
||||||
)
|
)
|
||||||
projects_with_info = result.all()
|
projects_with_info = result.all()
|
||||||
|
|
||||||
|
projects = [project for project, _, _ in projects_with_info]
|
||||||
|
member_counts = await attach_member_counts(db, projects)
|
||||||
|
|
||||||
projects_data = []
|
projects_data = []
|
||||||
for project, owner, member in projects_with_info:
|
for project, owner, member in projects_with_info:
|
||||||
project_dict = ProjectResponse.from_orm(project).dict()
|
project_dict = ProjectResponse.from_orm(project).dict()
|
||||||
|
|
@ -132,6 +151,7 @@ async def get_shared_projects(
|
||||||
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)
|
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)
|
projects_data.append(project_dict)
|
||||||
|
|
||||||
return success_response(data=projects_data)
|
return success_response(data=projects_data)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
--toolbar-bg: #fafafa;
|
--toolbar-bg: #fafafa;
|
||||||
--link-color: #1677ff;
|
--link-color: #1677ff;
|
||||||
--panel-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
|
--panel-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
|
||||||
|
--selected-bg: rgba(22, 119, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark {
|
body.dark {
|
||||||
|
|
@ -42,6 +43,7 @@ body.dark {
|
||||||
--toolbar-bg: #1f1f1f;
|
--toolbar-bg: #1f1f1f;
|
||||||
--link-color: #177ddc;
|
--link-color: #177ddc;
|
||||||
--panel-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
|
--panel-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
|
||||||
|
--selected-bg: rgba(23, 125, 220, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -166,14 +166,36 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 选中的文件夹样式 */
|
/* ===== 选中节点:文字、图标与背景同步高亮 ===== */
|
||||||
|
|
||||||
|
/* 选中叶子节点(文件)的整行背景 */
|
||||||
|
.file-tree .ant-menu-item-selected {
|
||||||
|
background-color: var(--selected-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中文件夹标题的背景 */
|
||||||
.file-tree .folder-selected>.ant-menu-submenu-title {
|
.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;
|
color: var(--link-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tree .folder-selected>.ant-menu-submenu-title:hover {
|
.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,
|
.file-tree .ant-menu-item,
|
||||||
|
|
|
||||||
|
|
@ -1019,14 +1019,11 @@ function DocumentEditor() {
|
||||||
|
|
||||||
if (!node.isLeaf) {
|
if (!node.isLeaf) {
|
||||||
const isOpen = openKeys.includes(node.key)
|
const isOpen = openKeys.includes(node.key)
|
||||||
const folderIconStyle = isSelected ? { color: '#1890ff' } : undefined
|
// 目录 - 选中样式(背景/文字/图标)统一由 CSS 类控制,保证文字与图标同步高亮
|
||||||
// 目录 - 通过className和style控制选中样式
|
|
||||||
return {
|
return {
|
||||||
key: node.key,
|
key: node.key,
|
||||||
label: labelContent,
|
label: labelContent,
|
||||||
icon: isOpen
|
icon: isOpen ? <FolderOpenOutlined /> : <FolderOutlined />,
|
||||||
? <FolderOpenOutlined style={folderIconStyle} />
|
|
||||||
: <FolderOutlined style={folderIconStyle} />,
|
|
||||||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||||
className: isSelected ? 'folder-selected' : '',
|
className: isSelected ? 'folder-selected' : '',
|
||||||
onTitleClick: () => {
|
onTitleClick: () => {
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,121 @@
|
||||||
font-size: 14px;
|
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 {
|
.markdown-body {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ function DocumentPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectFolder = (folderPath, { syncUrl = false } = {}) => {
|
const selectFolder = (folderPath, { syncUrl = false, expandSelf = false } = {}) => {
|
||||||
setSelectedFile('')
|
setSelectedFile('')
|
||||||
setSelectedNodeKey(folderPath)
|
setSelectedNodeKey(folderPath)
|
||||||
setMarkdownContent('')
|
setMarkdownContent('')
|
||||||
|
|
@ -136,11 +136,28 @@ function DocumentPage() {
|
||||||
setViewMode('folder')
|
setViewMode('folder')
|
||||||
expandParentFolders(`${folderPath}/placeholder`)
|
expandParentFolders(`${folderPath}/placeholder`)
|
||||||
|
|
||||||
|
// 从内容区点击进入子文件夹时,同时展开自身,保证与左侧文件树同步
|
||||||
|
if (expandSelf) {
|
||||||
|
setOpenKeys(prev => (prev.includes(folderPath) ? prev : [...prev, folderPath]))
|
||||||
|
}
|
||||||
|
|
||||||
if (syncUrl) {
|
if (syncUrl) {
|
||||||
updateSelectedParam(folderPath, false)
|
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 } = {}) => {
|
const openDocumentPath = (filePath, { syncUrl = false } = {}) => {
|
||||||
setSelectedFile(filePath)
|
setSelectedFile(filePath)
|
||||||
setSelectedNodeKey(filePath)
|
setSelectedNodeKey(filePath)
|
||||||
|
|
@ -1257,10 +1274,75 @@ function DocumentPage() {
|
||||||
toolbarTarget={pdfToolbarTarget}
|
toolbarTarget={pdfToolbarTarget}
|
||||||
/>
|
/>
|
||||||
) : viewMode === 'folder' ? (
|
) : viewMode === 'folder' ? (
|
||||||
<div className="docs-folder-placeholder">
|
(() => {
|
||||||
<FolderOpenOutlined />
|
const { folders, files, node } = getFolderChildren(selectedNodeKey)
|
||||||
<span>已选择文件夹,请从左侧选择 Markdown 或 PDF 文件查看。</span>
|
if (!node || node.isLeaf) {
|
||||||
</div>
|
return (
|
||||||
|
<div className="docs-folder-placeholder">
|
||||||
|
<FolderOpenOutlined />
|
||||||
|
<span>已选择文件夹,请从左侧选择 Markdown 或 PDF 文件查看。</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const total = folders.length + files.length
|
||||||
|
return (
|
||||||
|
<div className="docs-folder-view">
|
||||||
|
<div className="docs-folder-toolbar">
|
||||||
|
<div className="docs-folder-path" title={node.key}>
|
||||||
|
<FolderOpenOutlined className="docs-folder-path-icon" />
|
||||||
|
<span className="docs-folder-path-text">{node.key || '根目录'}</span>
|
||||||
|
</div>
|
||||||
|
<span className="docs-folder-counts">
|
||||||
|
{folders.length} 个文件夹 · {files.length} 个文档
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{total === 0 ? (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description="该文件夹为空"
|
||||||
|
style={{ padding: '48px 0' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="docs-folder-grid">
|
||||||
|
{folders.map(sub => (
|
||||||
|
<div
|
||||||
|
key={sub.key}
|
||||||
|
className="docs-folder-item"
|
||||||
|
onClick={() => selectFolder(sub.key, { syncUrl: true, expandSelf: true })}
|
||||||
|
title={sub.title}
|
||||||
|
>
|
||||||
|
<FolderOutlined className="docs-folder-item-icon" />
|
||||||
|
<span className="docs-folder-item-name">{sub.title}</span>
|
||||||
|
<span className="docs-folder-item-count">{sub.children?.length || 0} 项</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{files.map(file => {
|
||||||
|
const isPdf = file.title.toLowerCase().endsWith('.pdf')
|
||||||
|
const displayName = isPdf ? file.title : file.title.replace(/.md$/i, '')
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={file.key}
|
||||||
|
className="docs-folder-item docs-file-item"
|
||||||
|
onClick={() => openDocumentPath(file.key, { syncUrl: true })}
|
||||||
|
title={file.title}
|
||||||
|
>
|
||||||
|
{isPdf
|
||||||
|
? <FilePdfOutlined className="docs-folder-item-icon pdf" />
|
||||||
|
: <FileTextOutlined className="docs-folder-item-icon md" />}
|
||||||
|
<span className="docs-folder-item-name">
|
||||||
|
{displayName}
|
||||||
|
{file.is_shared && <ShareAltOutlined className="docs-folder-item-share" title="已分享" />}
|
||||||
|
</span>
|
||||||
|
<span className="docs-folder-item-count">{isPdf ? 'PDF 文档' : 'Markdown 文档'}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()
|
||||||
) : isLargeMarkdown ? (
|
) : isLargeMarkdown ? (
|
||||||
<LargeMarkdownViewer
|
<LargeMarkdownViewer
|
||||||
ref={largeMarkdownRef}
|
ref={largeMarkdownRef}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,11 @@
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== 项目卡片(统一骨架 + 固定高度) ========== */
|
||||||
.project-card {
|
.project-card {
|
||||||
text-align: center;
|
height: 236px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -35,6 +38,18 @@
|
||||||
box-shadow: var(--panel-shadow);
|
box-shadow: var(--panel-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-card .ant-card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px 18px 14px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card .ant-card-actions {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.project-card-public-badge {
|
.project-card-public-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
|
@ -49,25 +64,67 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-icon {
|
/* 卡片主体:标题在上、描述占中间弹性空间、元信息贴底 */
|
||||||
margin-bottom: 16px;
|
.project-card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-title-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 9px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 17px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-title-icon.is-owner {
|
||||||
|
background: linear-gradient(135deg, #1677ff 0%, #69b1ff 100%);
|
||||||
|
box-shadow: 0 3px 8px rgba(22, 119, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .project-card-title-icon.is-owner {
|
||||||
|
background: linear-gradient(135deg, #1668dc 0%, #3c89e8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-title-icon.is-shared {
|
||||||
|
background: linear-gradient(135deg, #52c41a 0%, #95de64 100%);
|
||||||
|
box-shadow: 0 3px 8px rgba(82, 196, 26, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card h3 {
|
.project-card h3 {
|
||||||
font-size: 16px;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 描述为卡片主要信息,占据剩余空间保证高度一致 */
|
||||||
.project-description {
|
.project-description {
|
||||||
font-size: 14px;
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
margin-bottom: 12px;
|
margin: 0 0 10px;
|
||||||
min-height: 40px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
@ -75,9 +132,151 @@
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-meta {
|
/* 底部元信息区:三项信息三等分网格(三分格,无分隔线) */
|
||||||
font-size: 12px;
|
.project-card-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 6px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px dashed var(--border-color);
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每格内容水平垂直居中,超长省略 */
|
||||||
|
.project-card-meta > * {
|
||||||
|
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;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 圆点分页指示器样式 */
|
/* ========== 圆点分页指示器(对称布局) ========== */
|
||||||
.dot-pagination.ant-pagination {
|
.dot-pagination.ant-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
border: none !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
min-width: 16px !important;
|
margin: 0 !important;
|
||||||
height: 16px !important;
|
min-width: 24px !important;
|
||||||
line-height: 16px !important;
|
height: 24px !important;
|
||||||
margin: 0 4px !important;
|
line-height: 24px !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot-pagination .ant-pagination-item a {
|
.dot-pagination .ant-pagination-item a {
|
||||||
|
|
@ -132,19 +340,67 @@ body.dark .kb-warning-banner {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--text-color-secondary);
|
background-color: var(--text-color-secondary);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
margin: 4px auto;
|
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot-pagination .ant-pagination-item-active .pagination-dot {
|
.dot-pagination .ant-pagination-item-active .pagination-dot {
|
||||||
background-color: var(--link-color);
|
background-color: var(--link-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1.2);
|
transform: scale(1.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot-pagination .ant-pagination-prev,
|
.dot-pagination .ant-pagination-jump-prev,
|
||||||
.dot-pagination .ant-pagination-next {
|
.dot-pagination .ant-pagination-jump-next {
|
||||||
|
border: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
margin: 0 !important;
|
||||||
min-width: 24px !important;
|
min-width: 24px !important;
|
||||||
height: 24px !important;
|
height: 24px !important;
|
||||||
line-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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 { 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 { getMyProjects, getOwnedProjects, getSharedProjects, createProject, deleteProject, updateProject, getProjectMembers, addProjectMember, removeProjectMember, getGitRepos, createGitRepo, updateGitRepo, deleteGitRepo, transferProject } from '@/api/project'
|
||||||
import { getProjectShareInfo, updateProjectShareSettings } from '@/api/share'
|
import { getProjectShareInfo, updateProjectShareSettings } from '@/api/share'
|
||||||
import { getUserList } from '@/api/users'
|
import { getUserList } from '@/api/users'
|
||||||
|
|
@ -9,6 +9,7 @@ import { searchDocuments } from '@/api/search'
|
||||||
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'
|
||||||
|
import useUserStore from '@/stores/userStore'
|
||||||
import './ProjectList.css'
|
import './ProjectList.css'
|
||||||
|
|
||||||
function ProjectList({ type = 'my' }) {
|
function ProjectList({ type = 'my' }) {
|
||||||
|
|
@ -36,9 +37,62 @@ function ProjectList({ type = 'my' }) {
|
||||||
const [transferModalVisible, setTransferModalVisible] = useState(false)
|
const [transferModalVisible, setTransferModalVisible] = useState(false)
|
||||||
const [transferForm] = Form.useForm()
|
const [transferForm] = Form.useForm()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { user: currentUser } = useUserStore()
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const pageSize = 8
|
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 <CrownOutlined />
|
||||||
|
if (project.user_role === 'editor') return <EditOutlined />
|
||||||
|
return <EyeOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
kbModalVisible,
|
kbModalVisible,
|
||||||
progress,
|
progress,
|
||||||
|
|
@ -620,7 +674,7 @@ function ProjectList({ type = 'my' }) {
|
||||||
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
|
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
className="project-card"
|
className={type === 'share' ? 'project-card project-card-share' : 'project-card project-card-my'}
|
||||||
onClick={() => handleOpenProject(project.id)}
|
onClick={() => handleOpenProject(project.id)}
|
||||||
actions={type === 'my' ? [
|
actions={type === 'my' ? [
|
||||||
<Tooltip key="settings" title="项目设置"><SettingOutlined onClick={(e) => handleEdit(e, project)} /></Tooltip>,
|
<Tooltip key="settings" title="项目设置"><SettingOutlined onClick={(e) => handleEdit(e, project)} /></Tooltip>,
|
||||||
|
|
@ -628,30 +682,53 @@ function ProjectList({ type = 'my' }) {
|
||||||
<Tooltip key="kb" title="知识库向量化"><DatabaseOutlined onClick={(e) => handleKnowledge(e, project)} /></Tooltip>,
|
<Tooltip key="kb" title="知识库向量化"><DatabaseOutlined onClick={(e) => handleKnowledge(e, project)} /></Tooltip>,
|
||||||
<Tooltip key="members" title="成员管理"><TeamOutlined onClick={(e) => handleMembers(e, project)} /></Tooltip>,
|
<Tooltip key="members" title="成员管理"><TeamOutlined onClick={(e) => handleMembers(e, project)} /></Tooltip>,
|
||||||
] : [
|
] : [
|
||||||
<EyeOutlined key="view" />,
|
<Tooltip key="view" title="进入项目"><EyeOutlined /></Tooltip>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{/* 公开项目标识 */}
|
{/* 公开项目标识 */}
|
||||||
{project.is_public === 1 && (
|
{project.is_public === 1 && (
|
||||||
<div className="project-card-public-badge">公开</div>
|
<div className="project-card-public-badge">公开</div>
|
||||||
)}
|
)}
|
||||||
<div className="project-card-icon">
|
|
||||||
<FolderOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
{/* 统一的卡片骨架:标题 + 描述(主要信息)+ 底部统计/归属/角色 */}
|
||||||
</div>
|
<div className="project-card-body">
|
||||||
<h3>{project.name}</h3>
|
<div className="project-card-title-row">
|
||||||
<p className="project-description">{project.description || '暂无描述'}</p>
|
<span className={`project-card-title-icon ${type === 'my' ? 'is-owner' : 'is-shared'}`}>
|
||||||
<div className="project-meta">
|
{type === 'my' ? <FolderOutlined /> : <TeamOutlined />}
|
||||||
<span>文档数: {project.doc_count || 0}</span>
|
|
||||||
{type === 'share' && project.owner_name && (
|
|
||||||
<span style={{ marginLeft: 12 }}>
|
|
||||||
所有者: {project.owner_nickname || project.owner_name}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<h3 title={project.name}>{project.name}</h3>
|
||||||
{type === 'share' && project.user_role && (
|
</div>
|
||||||
<span style={{ marginLeft: 12 }}>
|
<p className="project-description">{project.description || '暂无描述'}</p>
|
||||||
角色: {project.user_role === 'admin' ? '管理者' : project.user_role === 'editor' ? '编辑者' : '查看者'}
|
{/* 归属人信息 + 项目信息:单行展示 */}
|
||||||
</span>
|
<div className="project-card-meta">
|
||||||
)}
|
{type === 'my' ? (
|
||||||
|
<>
|
||||||
|
<span className="project-stat-item" title="文档数量">
|
||||||
|
<FileTextOutlined /> {project.doc_count || 0} 文档
|
||||||
|
</span>
|
||||||
|
<span className="project-stat-item" title="创建时间">
|
||||||
|
<CalendarOutlined /> {formatDate(project.created_at) || '—'}
|
||||||
|
</span>
|
||||||
|
<span className="project-stat-item" title="项目参与人数量">
|
||||||
|
<TeamOutlined /> {project.member_count ?? 0} 人参与
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="project-stat-item" title="文档数量">
|
||||||
|
<FileTextOutlined /> {project.doc_count || 0} 文档
|
||||||
|
</span>
|
||||||
|
<span className="project-card-owner" title={cardOwnerName(project)}>
|
||||||
|
<span className="project-card-owner-avatar">{cardOwnerInitial(project)}</span>
|
||||||
|
<span className="project-card-owner-name">{cardOwnerName(project)}</span>
|
||||||
|
</span>
|
||||||
|
<span className={`project-card-role-badge ${cardRoleClass(project)}`} title={cardRoleLabel(project)}>
|
||||||
|
{cardRoleIcon(project)}
|
||||||
|
{cardRoleLabel(project)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue