feat(mindspace): add per-user OA cloud drive with fds-style file manager

Expose authenticated /api/mindspace/v1/oa/drive APIs over each user's oa/ workspace and wire a MindSpace entry at /space/oa/drive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-10 18:55:46 +08:00
parent d07815d317
commit 6d5eb15b1b
22 changed files with 3833 additions and 9 deletions
+21 -9
View File
@@ -512,6 +512,7 @@ type MindSpaceRouteSync = {
pushWechatConfig: () => void;
pushCategory: (code: MindSpaceSaveCategory | MindSpaceCategory['code']) => void;
pushPage: (pageId: string) => void;
pushOaDrive?: () => void;
};
function MindSpaceBalanceRing() {
@@ -2862,15 +2863,26 @@ export function MindSpaceView({
</p>
<h2>{selectedCategory.name}</h2>
</div>
{['oa', 'public', 'health'].includes(selectedCategory.code) && (
<button
type="button"
className="mindspace-primary"
onClick={() => openUpload(selectedCategory)}
>
{selectedCategory.code === 'health' ? '上传报告' : '上传资料'}
</button>
)}
<div className="mindspace-category-actions">
{selectedCategory.code === 'oa' && routeSync?.pushOaDrive && (
<button
type="button"
className="mindspace-secondary"
onClick={() => routeSync.pushOaDrive?.()}
>
</button>
)}
{['oa', 'public', 'health'].includes(selectedCategory.code) && (
<button
type="button"
className="mindspace-primary"
onClick={() => openUpload(selectedCategory)}
>
{selectedCategory.code === 'health' ? '上传报告' : '上传资料'}
</button>
)}
</div>
</div>
{selectedCategory.code === 'health' ? (
+28
View File
@@ -12898,3 +12898,31 @@ body,
max-width: 58%;
}
}
.mindspace-category-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.oa-drive-shell {
display: flex;
flex-direction: column;
height: 100dvh;
background: #f4f1ea;
}
.oa-drive-toolbar {
flex: 0 0 auto;
padding: 10px 16px;
border-bottom: 1px solid rgba(24, 33, 29, 0.08);
background: #fffaf0;
}
.oa-drive-frame {
flex: 1 1 auto;
width: 100%;
border: 0;
background: #fff;
}
+7
View File
@@ -1,5 +1,6 @@
import { matchPath, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { MindSpaceView } from '../components/MindSpaceView';
import { OaDriveRoute } from './OaDriveRoute';
import type { MindSpaceSaveCategory, PortalUser } from '../types';
const CATEGORY_CODES = new Set<MindSpaceSaveCategory | 'health'>(['draft', 'oa', 'private', 'public', 'health']);
@@ -28,9 +29,14 @@ export function MindSpaceRoute({
const pageMatch = matchPath('/space/page/:pageId', location.pathname);
const achievementsMatch = matchPath('/space/achievements', location.pathname);
const wechatConfigMatch = matchPath('/space/wechat-config', location.pathname);
const oaDriveMatch = matchPath('/space/oa/drive', location.pathname);
const pageId = pageMatch?.params.pageId ?? null;
const categoryCode = parseCategory(searchParams.get('category'), healthEnabled);
if (oaDriveMatch) {
return <OaDriveRoute user={user} />;
}
return (
<MindSpaceView
user={user}
@@ -48,6 +54,7 @@ export function MindSpaceRoute({
pushWechatConfig: () => navigate('/space/wechat-config'),
pushCategory: (code) => navigate(`/space?category=${code}`),
pushPage: (id) => navigate(`/space/page/${id}`),
pushOaDrive: () => navigate('/space/oa/drive'),
}}
/>
);
+29
View File
@@ -0,0 +1,29 @@
import { Navigate, useNavigate } from 'react-router-dom';
import type { PortalUser } from '../types';
export function OaDriveRoute({
user,
}: {
user: PortalUser | null;
}) {
const navigate = useNavigate();
if (!user) {
return <Navigate to="/?return_to=/space/oa/drive" replace />;
}
return (
<div className="oa-drive-shell">
<div className="oa-drive-toolbar">
<button type="button" className="mindspace-inline-back" onClick={() => navigate('/space?category=oa')}>
OA
</button>
</div>
<iframe
className="oa-drive-frame"
title="OA 网盘"
src="/oa-drive/index.html"
/>
</div>
);
}