优化
This commit is contained in:
@@ -10,7 +10,7 @@ export const useTeamMembership = (teamId) => {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await SupabaseService.select('team_memberships', {
|
const result = await supabaseService.select('team_memberships', {
|
||||||
select: '*',
|
select: '*',
|
||||||
relations: {
|
relations: {
|
||||||
user: 'id, email, name'
|
user: 'id, email, name'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const useTeams = () => {
|
|||||||
// 获取团队列表
|
// 获取团队列表
|
||||||
const fetchTeams = async (params = {}) => {
|
const fetchTeams = async (params = {}) => {
|
||||||
try {
|
try {
|
||||||
const result = await SupabaseService.select('teams', {
|
const result = await supabaseService.select('teams', {
|
||||||
select: `
|
select: `
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const NotFound = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
key="home"
|
key="home"
|
||||||
onClick={() => navigate('/dashboard')}
|
onClick={() => navigate('/company/serviceTeamplate')}
|
||||||
>
|
>
|
||||||
返回首页
|
返回首页
|
||||||
</Button>,
|
</Button>,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
FolderOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import MonacoEditor from "@monaco-editor/react";
|
import MonacoEditor from "@monaco-editor/react";
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||||
@@ -70,6 +71,8 @@ const StorageManager = () => {
|
|||||||
const LOAD_MORE_SIZE = 100; // 每次加载100条
|
const LOAD_MORE_SIZE = 100; // 每次加载100条
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
const [newFileName, setNewFileName] = useState("");
|
const [newFileName, setNewFileName] = useState("");
|
||||||
|
const [currentPath, setCurrentPath] = useState(""); // 添加当前路径状态
|
||||||
|
const [pathHistory, setPathHistory] = useState([]); // 添加路径历史记录
|
||||||
|
|
||||||
// 文件图标映射
|
// 文件图标映射
|
||||||
const getFileIcon = (file) => {
|
const getFileIcon = (file) => {
|
||||||
@@ -100,19 +103,29 @@ const StorageManager = () => {
|
|||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.storage
|
const { data, error } = await supabase.storage
|
||||||
.from("file")
|
.from("file")
|
||||||
.list("", {
|
.list(currentPath, { // 使用当前路径
|
||||||
limit: isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE,
|
limit: isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE,
|
||||||
offset: isInitial ? 0 : displayFiles.length,
|
offset: isInitial ? 0 : displayFiles.length,
|
||||||
sortBy: { column: "created_at", order: "desc" },
|
sortBy: { column: "created_at", order: "desc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
if (isInitial) {
|
|
||||||
setDisplayFiles(data || []);
|
// 对数据进行排序,文件夹在前
|
||||||
} else {
|
const sortedData = (data || []).sort((a, b) => {
|
||||||
setDisplayFiles(prev => [...prev, ...(data || [])]);
|
// 首先按照类型排序(文件夹在前)
|
||||||
}
|
if (a.metadata?.isFolder !== b.metadata?.isFolder) {
|
||||||
|
return a.metadata?.isFolder ? -1 : 1;
|
||||||
|
}
|
||||||
|
// 然后按照时间排序
|
||||||
|
return new Date(b.created_at) - new Date(a.created_at);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isInitial) {
|
||||||
|
setDisplayFiles(sortedData);
|
||||||
|
} else {
|
||||||
|
setDisplayFiles(prev => [...prev, ...sortedData]);
|
||||||
|
}
|
||||||
|
|
||||||
setHasMore(data?.length === (isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE));
|
setHasMore(data?.length === (isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -208,9 +221,11 @@ const StorageManager = () => {
|
|||||||
if (selectedType === "全部") return matchesSearch;
|
if (selectedType === "全部") return matchesSearch;
|
||||||
|
|
||||||
const mimetype = file.metadata?.mimetype || '';
|
const mimetype = file.metadata?.mimetype || '';
|
||||||
return matchesType && FILE_TYPES[selectedType]?.some(type =>
|
const matchesType = FILE_TYPES[selectedType]?.some(type =>
|
||||||
mimetype.startsWith(type) || mimetype === type
|
mimetype.startsWith(type) || mimetype === type
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return matchesSearch && matchesType;
|
||||||
});
|
});
|
||||||
}, [displayFiles, searchText, selectedType]);
|
}, [displayFiles, searchText, selectedType]);
|
||||||
|
|
||||||
@@ -258,7 +273,7 @@ const StorageManager = () => {
|
|||||||
fetchAllFiles();
|
fetchAllFiles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 判断是否是图片
|
// 判断是否是<EFBFBD><EFBFBD>片
|
||||||
const isImage = (file) => {
|
const isImage = (file) => {
|
||||||
return file.metadata?.mimetype?.startsWith("image/");
|
return file.metadata?.mimetype?.startsWith("image/");
|
||||||
};
|
};
|
||||||
@@ -349,12 +364,21 @@ const StorageManager = () => {
|
|||||||
try {
|
try {
|
||||||
const { error } = await supabase.storage.from("file").remove([fileName]);
|
const { error } = await supabase.storage.from("file").remove([fileName]);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
message.success("文件删除成功");
|
message.success("文件删除成功");
|
||||||
fetchAllFiles();
|
|
||||||
|
// 直接从本地状态中移除被删除的文件
|
||||||
|
setDisplayFiles(prev => prev.filter(file => file.name !== fileName));
|
||||||
|
|
||||||
|
// 如果删除的是当前选中的文件,清空预览
|
||||||
if (selectedFile?.name === fileName) {
|
if (selectedFile?.name === fileName) {
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setFileContent("");
|
setFileContent("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新文件类型统计
|
||||||
|
// 注意:typeStats 是通过 useMemo 自动计算的,不需要手动更新
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(`删除失败: ${error.message}`);
|
message.error(`删除失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -367,7 +391,7 @@ const StorageManager = () => {
|
|||||||
|
|
||||||
// 加载更多数据
|
// 加载更多数据
|
||||||
const loadMoreFiles = () => {
|
const loadMoreFiles = () => {
|
||||||
if (!hasMore || loading || hasFilters) return; // 有过滤条件<EFBFBD><EFBFBD>不加载更多
|
if (!hasMore || loading || hasFilters) return; // 有过滤条件不加载更多
|
||||||
fetchAllFiles(false);
|
fetchAllFiles(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -406,21 +430,64 @@ const StorageManager = () => {
|
|||||||
return '其他';
|
return '其他';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染文件列表
|
// 渲染加载状态
|
||||||
|
const LoadingSpinner = () => (
|
||||||
|
<div className="flex justify-center items-center p-4">
|
||||||
|
<div className="w-6 h-6 border-2 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理文件夹点击
|
||||||
|
const handleFolderClick = (folderName) => {
|
||||||
|
const newPath = currentPath ? `${currentPath}/${folderName}` : folderName;
|
||||||
|
setPathHistory(prev => [...prev, currentPath]);
|
||||||
|
setCurrentPath(newPath);
|
||||||
|
setDisplayFiles([]); // 清空当前列表
|
||||||
|
setHasMore(true); // 重置加载更多状态
|
||||||
|
fetchAllFiles(true); // 重新获取文件列表
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回上一级
|
||||||
|
const handleBack = () => {
|
||||||
|
const previousPath = pathHistory[pathHistory.length - 1];
|
||||||
|
setPathHistory(prev => prev.slice(0, -1));
|
||||||
|
setCurrentPath(previousPath);
|
||||||
|
setDisplayFiles([]); // 清空当前列表
|
||||||
|
setHasMore(true); // 重置加载更多状态
|
||||||
|
fetchAllFiles(true); // 重新获取文件列表
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改文件列表渲染
|
||||||
const renderFileList = () => (
|
const renderFileList = () => (
|
||||||
<div className="flex-1 overflow-y-auto" id="scrollableDiv">
|
<div
|
||||||
|
className="flex-1 overflow-y-auto bg-white rounded-lg shadow-sm"
|
||||||
|
id="scrollableDiv"
|
||||||
|
>
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
{currentPath && (
|
||||||
|
<div className="p-4 border-b border-gray-100">
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={handleBack}
|
||||||
|
className="px-0"
|
||||||
|
>
|
||||||
|
返回上级
|
||||||
|
</Button>
|
||||||
|
<span className="text-gray-500">/</span>
|
||||||
|
<span className="text-gray-900">{currentPath}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
dataLength={displayFiles.length}
|
dataLength={displayFiles.length}
|
||||||
next={loadMoreFiles}
|
next={loadMoreFiles}
|
||||||
hasMore={!hasFilters && hasMore} // 只在没有过滤条件时显示加载更多
|
hasMore={!hasFilters && hasMore}
|
||||||
loader={
|
loader={<LoadingSpinner />}
|
||||||
<div className="text-center py-4">
|
|
||||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
scrollableTarget="scrollableDiv"
|
scrollableTarget="scrollableDiv"
|
||||||
endMessage={
|
endMessage={
|
||||||
<p className="text-center text-gray-500 py-4">
|
<p className="text-center text-gray-500 py-4 text-sm">
|
||||||
{displayFiles.length > 0
|
{displayFiles.length > 0
|
||||||
? hasFilters
|
? hasFilters
|
||||||
? "已显示所有匹配文件"
|
? "已显示所有匹配文件"
|
||||||
@@ -430,66 +497,80 @@ const StorageManager = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<List
|
<List
|
||||||
loading={loading && displayFiles.length === 0}
|
loading={false}
|
||||||
dataSource={filteredFiles}
|
dataSource={filteredFiles}
|
||||||
locale={{ emptyText: "暂无文件" }}
|
locale={{ emptyText: "暂无文件" }}
|
||||||
renderItem={(file) => (
|
renderItem={(file) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
className={`cursor-pointer hover:bg-gray-100 ${
|
className={`
|
||||||
selectedFile?.name === file.name ? "bg-blue-50" : ""
|
relative group cursor-pointer transition-colors duration-200
|
||||||
}`}
|
hover:bg-gray-50
|
||||||
onClick={() => previewFile(file)}
|
${selectedFile?.name === file.name ? "bg-blue-50" : ""}
|
||||||
actions={[
|
`}
|
||||||
<Popconfirm
|
onClick={() => file.metadata?.isFolder ? handleFolderClick(file.name) : previewFile(file)}
|
||||||
key="delete"
|
|
||||||
title="确认删除"
|
|
||||||
description={`是否确认删除文件 "${file.name}"?`}
|
|
||||||
onConfirm={(e) => {
|
|
||||||
e?.stopPropagation();
|
|
||||||
handleDelete(file.name);
|
|
||||||
}}
|
|
||||||
okText="确认"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={
|
avatar={
|
||||||
isImage(file) ? (
|
file.metadata?.isFolder ? (
|
||||||
|
<div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
<FolderOutlined className="text-xl" />
|
||||||
|
</div>
|
||||||
|
) : isImage(file) ? (
|
||||||
<Image
|
<Image
|
||||||
src={getFileUrl(file.name)}
|
src={getFileUrl(file.name)}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
width={32}
|
width={40}
|
||||||
height={32}
|
height={40}
|
||||||
className="object-cover rounded"
|
className="object-cover rounded-lg"
|
||||||
preview={false}
|
preview={false}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
placeholder={
|
placeholder={
|
||||||
<div className="w-8 h-8 bg-gray-200 rounded animate-pulse" />
|
<div className="w-10 h-10 bg-gray-100 rounded-lg animate-pulse" />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
getFileIcon(file)
|
<div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg text-gray-500">
|
||||||
|
{getFileIcon(file)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={file.name}
|
title={
|
||||||
|
<div className="flex items-center w-full pr-16">
|
||||||
|
<span className="text-sm font-medium text-gray-900 block w-full truncate group-hover:text-blue-500 transition-colors duration-200">
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
{!file.metadata?.isFolder && (
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除"
|
||||||
|
description={`是否确认删除${file.metadata?.isFolder ? '文件夹' : '文件'} "${file.name}"?`}
|
||||||
|
onConfirm={(e) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
handleDelete(currentPath ? `${currentPath}/${file.name}` : file.name);
|
||||||
|
}}
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="absolute right-4 invisible group-hover:visible transition-all duration-200"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500 space-x-4">
|
||||||
<span>类型: {file.metadata?.mimetype}</span>
|
<span>{file.metadata?.isFolder ? '文件夹' : `类型: ${file.metadata?.mimetype}`}</span>
|
||||||
<span className="ml-2">
|
{!file.metadata?.isFolder && (
|
||||||
大小: {(file.metadata?.size / 1024).toFixed(2)} KB
|
<>
|
||||||
</span>
|
<span>大小: {(file.metadata?.size / 1024).toFixed(2)} KB</span>
|
||||||
<span className="ml-2">
|
<span>创建时间: {new Date(file.created_at).toLocaleString()}</span>
|
||||||
创建时间: {new Date(file.created_at).toLocaleString()}
|
</>
|
||||||
</span>
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -519,51 +600,68 @@ const StorageManager = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50">
|
<div className="flex h-screen bg-gray-50">
|
||||||
{/* 左侧文件列表 */}
|
{/* 左侧文件列表 */}
|
||||||
<div className="w-1/3 p-2 border-r border-gray-200 flex flex-col">
|
<div className="w-1/3 p-4 flex flex-col space-y-4">
|
||||||
<div className="mb-4 h-[150px]">
|
{/* 上传区域 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||||
<Dragger
|
<Dragger
|
||||||
{...uploadProps}
|
{...uploadProps}
|
||||||
className="bg-white p-2 rounded-lg shadow-sm"
|
className="px-6 py-8 hover:bg-gray-50 transition-colors group"
|
||||||
>
|
>
|
||||||
<p className="text-base">点击或者拖拽文件到此区域上传</p>
|
<div className="space-y-3 text-center">
|
||||||
<p className="ant-upload-hint text-xs text-gray-500">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-50 group-hover:bg-blue-100 transition-colors">
|
||||||
支持单个或批量上传,文件大小不超过50MB
|
<InboxOutlined className="text-3xl text-blue-500" />
|
||||||
</p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-base font-medium text-gray-900">
|
||||||
|
点击或拖拽文件上传
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
支持单个或批量上传,文件大小不超过50MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dragger>
|
</Dragger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索和筛选区域 */}
|
{/* 搜索和筛选区域 */}
|
||||||
<div className="mb-4 space-y-3">
|
<div className="space-y-3">
|
||||||
<Search
|
<Search
|
||||||
placeholder="搜索文件名..."
|
placeholder="搜索文件名..."
|
||||||
allowClear
|
allowClear
|
||||||
onChange={(e) => {
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
setSearchText(e.target.value);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
{renderTypeTags()}
|
<div className="bg-white p-3 rounded-lg shadow-sm">
|
||||||
|
{renderTypeTags()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 文件列表 */}
|
{/* 文件列表 */}
|
||||||
{renderFileList()}
|
{loading && displayFiles.length === 0 ? (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderFileList()
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧预览区域 */}
|
{/* 右侧预览区域 */}
|
||||||
<div className="flex-1 p-4 overflow-y-auto bg-white">
|
<div className="flex-1 p-4 bg-white border-l border-gray-200">
|
||||||
{selectedFile ? (
|
{selectedFile ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4 border-b pb-4">
|
<div className="mb-4 pb-4 border-b border-gray-200">
|
||||||
<Space size="middle" align="center">
|
<Space size="middle" align="center">
|
||||||
<Space>
|
<Space>
|
||||||
<span className="font-medium text-lg">
|
<span className="text-lg font-medium text-gray-900">
|
||||||
{selectedFile.name}
|
{selectedFile.name}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={startRename}
|
onClick={startRename}
|
||||||
|
className="text-gray-500 hover:text-blue-500"
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
{isHtml(selectedFile) && (
|
{isHtml(selectedFile) && (
|
||||||
@@ -575,7 +673,11 @@ const StorageManager = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isImage(selectedFile) && (
|
{!isImage(selectedFile) && (
|
||||||
<Button type="primary" onClick={handleSaveContent}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSaveContent}
|
||||||
|
className="bg-blue-500 hover:bg-blue-600"
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -588,15 +690,15 @@ const StorageManager = () => {
|
|||||||
<Image
|
<Image
|
||||||
src={getFileUrl(selectedFile.name)}
|
src={getFileUrl(selectedFile.name)}
|
||||||
alt={selectedFile.name}
|
alt={selectedFile.name}
|
||||||
className="max-w-full max-h-[80vh] object-contain"
|
className="max-w-full max-h-[80vh] object-contain rounded-lg"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
preview={{
|
preview={{
|
||||||
toolbarRender: () => null, // 隐藏底部工具栏
|
toolbarRender: () => null,
|
||||||
maskClassName: "backdrop-blur-sm",
|
maskClassName: "backdrop-blur-sm",
|
||||||
}}
|
}}
|
||||||
placeholder={
|
placeholder={
|
||||||
<div className="w-full h-[80vh] bg-gray-100 rounded flex items-center justify-center">
|
<div className="w-full h-[80vh] bg-gray-50 rounded-lg flex items-center justify-center">
|
||||||
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -605,7 +707,7 @@ const StorageManager = () => {
|
|||||||
isPreview ? (
|
isPreview ? (
|
||||||
<iframe
|
<iframe
|
||||||
srcDoc={fileContent}
|
srcDoc={fileContent}
|
||||||
className="w-full h-[calc(100vh-200px)] border rounded"
|
className="w-full h-[calc(100vh-200px)] border rounded-lg"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
@@ -616,7 +718,10 @@ const StorageManager = () => {
|
|||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: true },
|
minimap: { enabled: true },
|
||||||
|
roundedSelection: true,
|
||||||
|
padding: { top: 16 },
|
||||||
}}
|
}}
|
||||||
|
className="rounded-lg overflow-hidden"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -628,14 +733,20 @@ const StorageManager = () => {
|
|||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
|
roundedSelection: true,
|
||||||
|
padding: { top: 16 },
|
||||||
}}
|
}}
|
||||||
|
className="rounded-lg overflow-hidden"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full flex items-center justify-center text-gray-400">
|
<div className="h-full flex items-center justify-center text-gray-400">
|
||||||
选择文件以预览
|
<div className="text-center">
|
||||||
|
<InboxOutlined className="text-4xl mb-2" />
|
||||||
|
<p>选择文件以预览</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -654,6 +765,7 @@ const StorageManager = () => {
|
|||||||
onChange={(e) => setNewFileName(e.target.value)}
|
onChange={(e) => setNewFileName(e.target.value)}
|
||||||
placeholder="请输入新文件名"
|
placeholder="请输入新文件名"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
className="mt-4"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ExpandedMemberships = ({ teamId }) => {
|
|||||||
const loadMemberships = async () => {
|
const loadMemberships = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { data } = await SupabaseService.select('team_membership', {
|
const { data } = await supabaseService.select('team_membership', {
|
||||||
select: `
|
select: `
|
||||||
id,
|
id,
|
||||||
role,
|
role,
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
export const mockTeams = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: '研发团队',
|
|
||||||
avatarUrl: 'https://api.dicebear.com/7.x/avatars/svg?seed=1',
|
|
||||||
tags: ['前端', '后端', '设计'],
|
|
||||||
memberships: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
user: {
|
|
||||||
email: 'john@example.com',
|
|
||||||
name: 'John Doe'
|
|
||||||
},
|
|
||||||
role: 'OWNER',
|
|
||||||
isCreator: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
user: {
|
|
||||||
email: 'jane@example.com',
|
|
||||||
name: 'Jane Smith'
|
|
||||||
},
|
|
||||||
role: 'ADMIN',
|
|
||||||
isCreator: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
user: {
|
|
||||||
email: 'bob@example.com',
|
|
||||||
name: 'Bob Wilson'
|
|
||||||
},
|
|
||||||
role: 'MEMBER',
|
|
||||||
isCreator: false,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: '设计团队',
|
|
||||||
avatarUrl: 'https://api.dicebear.com/7.x/avatars/svg?seed=2',
|
|
||||||
tags: ['UI', 'UX', '平面设计'],
|
|
||||||
memberships: [
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
user: {
|
|
||||||
email: 'alice@example.com',
|
|
||||||
name: 'Alice Johnson'
|
|
||||||
},
|
|
||||||
role: 'OWNER',
|
|
||||||
isCreator: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
user: {
|
|
||||||
email: 'charlie@example.com',
|
|
||||||
name: 'Charlie Brown'
|
|
||||||
},
|
|
||||||
role: 'MEMBER',
|
|
||||||
isCreator: false,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { message } from 'antd';
|
|
||||||
|
|
||||||
export const useMemberActions = (teamId) => {
|
|
||||||
const handleUpdate = (id, values) => {
|
|
||||||
// 模拟API调用
|
|
||||||
console.log('Update member:', id, values);
|
|
||||||
message.success('成员信息已更新');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
|
||||||
// 模拟API调用
|
|
||||||
console.log('Delete member:', id);
|
|
||||||
message.success('成员已删除');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = (values) => {
|
|
||||||
// 模拟API调用
|
|
||||||
console.log('Add member:', values);
|
|
||||||
message.success('成员已添加');
|
|
||||||
return {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
teamId,
|
|
||||||
isCreator: false,
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleUpdate,
|
|
||||||
handleDelete,
|
|
||||||
handleAdd,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { message } from 'antd';
|
|
||||||
|
|
||||||
export const useTeamActions = () => {
|
|
||||||
const handleUpdate = (id, values) => {
|
|
||||||
// 模拟API调用
|
|
||||||
console.log('Update team:', id, values);
|
|
||||||
message.success('团队信息已更新');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
|
||||||
// 模拟API调用
|
|
||||||
console.log('Delete team:', id);
|
|
||||||
message.success('团队已删除');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = (values) => {
|
|
||||||
// 模拟API调用
|
|
||||||
console.log('Add team:', values);
|
|
||||||
message.success('团队已创建');
|
|
||||||
return {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleUpdate,
|
|
||||||
handleDelete,
|
|
||||||
handleAdd,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { useState, useCallback } from 'react';
|
|
||||||
import { supabase } from '@/config/supabase';
|
|
||||||
|
|
||||||
export const useTeamData = () => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadData = useCallback(async ({
|
|
||||||
pagination = {},
|
|
||||||
filters = {},
|
|
||||||
sorter = {},
|
|
||||||
search = ''
|
|
||||||
} = {}) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
let query = supabase
|
|
||||||
.from('teams')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
memberships:team_memberships(
|
|
||||||
id,
|
|
||||||
role,
|
|
||||||
user:users(id, email, name)
|
|
||||||
),
|
|
||||||
tags(id, name)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Add search condition
|
|
||||||
if (search) {
|
|
||||||
query = query.ilike('name', `%${search}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sorting
|
|
||||||
if (sorter.field) {
|
|
||||||
const order = sorter.order === 'descend' ? 'desc' : 'asc';
|
|
||||||
query = query.order(sorter.field, { ascending: order === 'asc' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pagination
|
|
||||||
const from = ((pagination.current || 1) - 1) * (pagination.pageSize || 10);
|
|
||||||
const to = from + (pagination.pageSize || 10) - 1;
|
|
||||||
query = query.range(from, to);
|
|
||||||
|
|
||||||
const { data: teams, count } = await query;
|
|
||||||
|
|
||||||
setData(teams || []);
|
|
||||||
setPagination({
|
|
||||||
...pagination,
|
|
||||||
total: count || 0,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch teams:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const createTeam = async (values) => {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.insert([values])
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTeam = async (id, values) => {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.update(values)
|
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTeam = async (id) => {
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('teams')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
data,
|
|
||||||
pagination,
|
|
||||||
loadData,
|
|
||||||
createTeam,
|
|
||||||
updateTeam,
|
|
||||||
deleteTeam,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { useState, useCallback } from 'react';
|
|
||||||
import { supabase } from '@/config/supabase';
|
|
||||||
|
|
||||||
export const useTeamMembership = (teamId) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [memberships, setMemberships] = useState([]);
|
|
||||||
|
|
||||||
const loadMemberships = useCallback(async () => {
|
|
||||||
if (!teamId) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
user:users(id, email, name)
|
|
||||||
`)
|
|
||||||
.eq('teamId', teamId);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
setMemberships(data || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch memberships:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [teamId]);
|
|
||||||
|
|
||||||
const addMembership = async (values) => {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.insert([{ ...values, teamId }])
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
await loadMemberships();
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMembership = async (id, values) => {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.update(values)
|
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
await loadMemberships();
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMembership = async (id) => {
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('team_memberships')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
await loadMemberships();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
memberships,
|
|
||||||
loadMemberships,
|
|
||||||
addMembership,
|
|
||||||
updateMembership,
|
|
||||||
deleteMembership,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -51,15 +51,13 @@ const AppRoutes = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* 公开路由 */}
|
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={
|
element={
|
||||||
user ? <Navigate to="/dashboard" replace /> : <Login />
|
user ? <Navigate to="/company/serviceTeamplate" replace /> : <Login />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 受保护的路由 */}
|
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -68,16 +66,7 @@ const AppRoutes = () => {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route
|
|
||||||
path="dashboard"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<LoadingComponent />}>
|
|
||||||
<Dashboard />
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{renderRoutes(routes)}
|
{renderRoutes(routes)}
|
||||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
|
|
||||||
// Dashboard route
|
|
||||||
const dashboardRoute = {
|
|
||||||
path: "dashboard",
|
|
||||||
component: lazy(() => import("@/pages/Dashboard")),
|
|
||||||
name: "仪表盘",
|
|
||||||
icon: "dashboard",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resource Management routes
|
// Resource Management routes
|
||||||
const resourceRoutes = [
|
const resourceRoutes = [
|
||||||
@@ -109,7 +102,12 @@ const companyRoutes = [
|
|||||||
const marketingRoutes = [];
|
const marketingRoutes = [];
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
dashboardRoute,
|
// {
|
||||||
|
// path: "dashboard",
|
||||||
|
// component: lazy(() => import("@/pages/Dashboard")),
|
||||||
|
// name: "仪表盘",
|
||||||
|
// icon: "dashboard",
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
path: "resource",
|
path: "resource",
|
||||||
component: lazy(() => import("@/pages/resource")),
|
component: lazy(() => import("@/pages/resource")),
|
||||||
|
|||||||
Reference in New Issue
Block a user