优化界面
parent
416ef48395
commit
2dbff78698
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? <FolderOpenOutlined style={folderIconStyle} />
|
||||
: <FolderOutlined style={folderIconStyle} />,
|
||||
icon: isOpen ? <FolderOpenOutlined /> : <FolderOutlined />,
|
||||
children: node.children ? convertTreeToMenuItems(node.children) : [],
|
||||
className: isSelected ? 'folder-selected' : '',
|
||||
onTitleClick: () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' ? (
|
||||
<div className="docs-folder-placeholder">
|
||||
<FolderOpenOutlined />
|
||||
<span>已选择文件夹,请从左侧选择 Markdown 或 PDF 文件查看。</span>
|
||||
</div>
|
||||
(() => {
|
||||
const { folders, files, node } = getFolderChildren(selectedNodeKey)
|
||||
if (!node || node.isLeaf) {
|
||||
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 ? (
|
||||
<LargeMarkdownViewer
|
||||
ref={largeMarkdownRef}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,11 @@
|
|||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* ========== 项目卡片(统一骨架 + 固定高度) ========== */
|
||||
.project-card {
|
||||
text-align: center;
|
||||
height: 236px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
|
|
@ -35,6 +38,18 @@
|
|||
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 {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
|
|
@ -49,25 +64,67 @@
|
|||
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 {
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* 描述为卡片主要信息,占据剩余空间保证高度一致 */
|
||||
.project-description {
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
min-height: 40px;
|
||||
margin: 0 0 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
|
@ -75,9 +132,151 @@
|
|||
-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);
|
||||
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;
|
||||
}
|
||||
|
||||
/* 圆点分页指示器样式 */
|
||||
/* ========== 圆点分页指示器(对称布局) ========== */
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <CrownOutlined />
|
||||
if (project.user_role === 'editor') return <EditOutlined />
|
||||
return <EyeOutlined />
|
||||
}
|
||||
|
||||
const {
|
||||
kbModalVisible,
|
||||
progress,
|
||||
|
|
@ -620,7 +674,7 @@ function ProjectList({ type = 'my' }) {
|
|||
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
|
||||
<Card
|
||||
hoverable
|
||||
className="project-card"
|
||||
className={type === 'share' ? 'project-card project-card-share' : 'project-card project-card-my'}
|
||||
onClick={() => handleOpenProject(project.id)}
|
||||
actions={type === 'my' ? [
|
||||
<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="members" title="成员管理"><TeamOutlined onClick={(e) => handleMembers(e, project)} /></Tooltip>,
|
||||
] : [
|
||||
<EyeOutlined key="view" />,
|
||||
<Tooltip key="view" title="进入项目"><EyeOutlined /></Tooltip>,
|
||||
]}
|
||||
>
|
||||
{/* 公开项目标识 */}
|
||||
{project.is_public === 1 && (
|
||||
<div className="project-card-public-badge">公开</div>
|
||||
)}
|
||||
<div className="project-card-icon">
|
||||
<FolderOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
||||
</div>
|
||||
<h3>{project.name}</h3>
|
||||
<p className="project-description">{project.description || '暂无描述'}</p>
|
||||
<div className="project-meta">
|
||||
<span>文档数: {project.doc_count || 0}</span>
|
||||
{type === 'share' && project.owner_name && (
|
||||
<span style={{ marginLeft: 12 }}>
|
||||
所有者: {project.owner_nickname || project.owner_name}
|
||||
|
||||
{/* 统一的卡片骨架:标题 + 描述(主要信息)+ 底部统计/归属/角色 */}
|
||||
<div className="project-card-body">
|
||||
<div className="project-card-title-row">
|
||||
<span className={`project-card-title-icon ${type === 'my' ? 'is-owner' : 'is-shared'}`}>
|
||||
{type === 'my' ? <FolderOutlined /> : <TeamOutlined />}
|
||||
</span>
|
||||
)}
|
||||
{type === 'share' && project.user_role && (
|
||||
<span style={{ marginLeft: 12 }}>
|
||||
角色: {project.user_role === 'admin' ? '管理者' : project.user_role === 'editor' ? '编辑者' : '查看者'}
|
||||
</span>
|
||||
)}
|
||||
<h3 title={project.name}>{project.name}</h3>
|
||||
</div>
|
||||
<p className="project-description">{project.description || '暂无描述'}</p>
|
||||
{/* 归属人信息 + 项目信息:单行展示 */}
|
||||
<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>
|
||||
</Card>
|
||||
</Col>
|
||||
|
|
|
|||
Loading…
Reference in New Issue