no message

This commit is contained in:
DESKTOP-PB0N82B\admin 2026-01-30 11:17:02 +08:00
parent 725409a24b
commit 271c11b453
30 changed files with 1680 additions and 326 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://172.16.1.12" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://172.16.1.12" />
<title>AssetPro</title> <title>AssetPro</title>
<script type="module" crossorigin src="./assets/index-C1_JoRdR.js"></script> <script type="module" crossorigin src="./assets/index-Cst68Kk3.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Cnn4LfhN.css"> <link rel="stylesheet" crossorigin href="./assets/index-BAlDckV1.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

Binary file not shown.

Binary file not shown.

View File

@ -14,8 +14,8 @@ x64:
nodeModuleFilePatterns: [] nodeModuleFilePatterns: []
nsis: nsis:
script: |- script: |-
!include "D:\Project\AssetPro\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh" !include "D:\Project\AssetPro2\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
!addincludedir "D:\Project\AssetPro\node_modules\app-builder-lib\templates\nsis\include" !addincludedir "D:\Project\AssetPro2\node_modules\app-builder-lib\templates\nsis\include"
!macro _isUpdated _a _b _t _f !macro _isUpdated _a _b _t _f
${StdUtils.TestParameter} $R9 "updated" ${StdUtils.TestParameter} $R9 "updated"
StrCmp "$R9" "true" `${_t}` `${_f}` StrCmp "$R9" "true" `${_t}` `${_f}`
@ -87,7 +87,7 @@ nsis:
!insertmacro MUI_LANGUAGE "Vietnamese" !insertmacro MUI_LANGUAGE "Vietnamese"
!macroend !macroend
!include "C:\Users\admin\AppData\Local\Temp\t-PKnz4i\0-messages.nsh" !include "C:\Users\admin\AppData\Local\Temp\t-p3ldDi\0-messages.nsh"
!addplugindir /x86-unicode "C:\Users\admin\AppData\Local\electron-builder\Cache\nsis\nsis-resources-3.4.1\plugins\x86-unicode" !addplugindir /x86-unicode "C:\Users\admin\AppData\Local\electron-builder\Cache\nsis\nsis-resources-3.4.1\plugins\x86-unicode"
Var newStartMenuLink Var newStartMenuLink

Binary file not shown.

View File

@ -5,6 +5,8 @@ import AssetLibrary from '@/pages/assets/AssetLibrary'
import UploadResource from '@/pages/assets/UploadResource' import UploadResource from '@/pages/assets/UploadResource'
import ProjectLibrary from '@/pages/projects/ProjectLibrary' import ProjectLibrary from '@/pages/projects/ProjectLibrary'
import NewProject from '@/pages/projects/NewProject' import NewProject from '@/pages/projects/NewProject'
import ProfilePage from '@/pages/ProfilePage'
import FileLibrary from '@/pages/files/FileLibrary'
import { ToastProvider } from '@/components/ui/toast-provider' import { ToastProvider } from '@/components/ui/toast-provider'
import { DownloadManagerProvider } from '@/components/DownloadManager' import { DownloadManagerProvider } from '@/components/DownloadManager'
@ -26,8 +28,12 @@ function App() {
<HashRouter> <HashRouter>
<Routes> <Routes>
<Route path="/" element={<Login />} /> <Route path="/" element={<Login />} />
<Route path="/upload" element={<UploadResource />} /> {/* Full width pages with Header but no Sidebar */}
<Route path="/app/projects/new" element={<NewProject />} /> <Route element={<MainLayout showSidebar={false} />}>
<Route path="/upload" element={<UploadResource />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/app/projects/new" element={<NewProject />} />
</Route>
{/* Main Application Routes */} {/* Main Application Routes */}
<Route path="/app" element={<MainLayout />}> <Route path="/app" element={<MainLayout />}>
@ -38,7 +44,7 @@ function App() {
<Route path="prototypes" element={<PlaceholderPage title="原型库" />} /> <Route path="prototypes" element={<PlaceholderPage title="原型库" />} />
<Route path="design" element={<PlaceholderPage title="设计库" />} /> <Route path="design" element={<PlaceholderPage title="设计库" />} />
<Route path="projects" element={<ProjectLibrary />} /> <Route path="projects" element={<ProjectLibrary />} />
<Route path="files" element={<PlaceholderPage title="文件库" />} /> <Route path="files" element={<FileLibrary />} />
<Route path="cases" element={<PlaceholderPage title="案例库" />} /> <Route path="cases" element={<PlaceholderPage title="案例库" />} />
<Route path="pending" element={<PlaceholderPage title="待审核" />} /> <Route path="pending" element={<PlaceholderPage title="待审核" />} />
</Route> </Route>

18
src/api/auth.ts Normal file
View File

@ -0,0 +1,18 @@
import simClient from './simulation';
export interface LoginParams {
username: string;
password?: string;
login_user_key?: string; // 示例中出现的字段,可能需要
}
export interface LoginResponse {
msg: string;
code: number;
data: any;
token: string;
}
export const login = (data: LoginParams) => {
return simClient.post<any, LoginResponse>('/login', data);
};

View File

@ -14,13 +14,34 @@ const apiClient = axios.create({
timeout: 60000, timeout: 60000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(API_TOKEN ? { Authorization: `token ${API_TOKEN}` } : {}),
}, },
}); });
// 请求拦截器,自动添加 token
apiClient.interceptors.request.use(
(config) => {
console.log(`>>> [Gitea] ${config.method?.toUpperCase()} ${config.url}`);
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`; // 这里的格式调整为 Bearer Token
console.log('--- Gitea Auth Token (Bearer):', token);
}
if (config.data) {
console.log('>>> Gitea Request JSON:', JSON.stringify(config.data, null, 2));
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器,用于错误处理 // 响应拦截器,用于错误处理
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => response, (response) => {
console.log('<<< Gitea Response JSON:', response.data);
return response;
},
(error) => { (error) => {
console.error('API Error:', error); console.error('API Error:', error);
return Promise.reject(error); return Promise.reject(error);

View File

@ -117,6 +117,32 @@ export const giteaApi = {
} }
}, },
/**
* Get Repository Branch Details (includes latest commit)
*/
getRepoBranch: async (owner: string, repo: string, branchName: string): Promise<any> => {
try {
const response = await apiClient.get(`/repos/${owner}/${repo}/branches/${branchName}`);
return response.data;
} catch (error) {
console.error(`Failed to fetch branch ${branchName} for ${owner}/${repo}`, error);
throw error;
}
},
/**
* Get all branches for a repository
*/
getRepoBranches: async (owner: string, repo: string): Promise<any[]> => {
try {
const response = await apiClient.get<any[]>(`/repos/${owner}/${repo}/branches`);
return response.data;
} catch (error) {
console.error(`Failed to fetch branches for ${owner}/${repo}`, error);
throw error;
}
},
/** /**
* Get Repository Commits * Get Repository Commits
*/ */

63
src/api/simulation.ts Normal file
View File

@ -0,0 +1,63 @@
import axios from 'axios';
import { AddSimulationResourceRequest, AddSimulationResourceResponse, GetSimulationListRequest, GetSimulationListResponse } from './simulationTypes';
// 仿真资源 API 基础路径
// 开发环境使用 Vite 代理,生产环境使用实际地址
export const SIM_API_BASE_URL = import.meta.env.DEV
? '/zichan-api'
: 'http://172.16.1.144:8081';
const simClient = axios.create({
baseURL: SIM_API_BASE_URL,
timeout: 60000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器,自动添加 token
simClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
console.log(`>>> [Simulation] ${config.method?.toUpperCase()} ${config.url}`);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log('--- Auth Token (Bearer):', token);
}
if (config.data) {
console.log('>>> Upload Request JSON:', JSON.stringify(config.data, null, 2));
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
simClient.interceptors.response.use(
(response) => {
console.log('<<< Received Response JSON:', response.data);
return response.data;
},
(error) => {
console.error('Simulation API Error:', error);
return Promise.reject(error);
}
);
/**
* 仿
*/
export const addSimulationResource = async (data: AddSimulationResourceRequest): Promise<AddSimulationResourceResponse> => {
return simClient.post('/zichan/sim/upload/add', data);
};
/**
*
*/
export const getSimulationUploadList = async (params: GetSimulationListRequest): Promise<GetSimulationListResponse> => {
return simClient.get('/zichan/sim/upload/list', { params });
};
export default simClient;

View File

@ -0,0 +1,62 @@
export interface ProUploadItem {
sourceName: string;
sourceForm: string;
fileType: string;
fileName: string;
fileUrl: string;
fileVersion: string;
}
export interface AddSimulationResourceRequest {
name: string;
stores?: string;
types: string;
category: string;
tag: string;
introduction: string;
details: string;
urls: string;
versions: string;
env: string;
createdBy: string;
proUploadList: ProUploadItem[];
}
export interface AddSimulationResourceResponse {
code: number;
msg: string;
data?: any;
}
export interface SimulationResource {
id: string;
name: string;
stores?: string;
types: string;
category: string;
tag: string;
introduction: string;
details: string;
urls: string;
versions: string;
env: string;
createdTime: string;
createdBy: string;
}
export interface GetSimulationListRequest {
pageNum?: number;
pageSize?: number;
name?: string;
types?: string;
category?: string;
tag?: string;
env?: string;
}
export interface GetSimulationListResponse {
code: number;
msg: string;
rows: SimulationResource[];
total: number;
}

View File

@ -14,7 +14,7 @@ interface DownloadItem {
} }
interface DownloadContextType { interface DownloadContextType {
startDownload: (repoUrl: string, targetDir: string) => Promise<void> startDownload: (repoUrl: string, targetDir: string, branch?: string) => Promise<void>
activeDownloads: DownloadItem[] activeDownloads: DownloadItem[]
} }
@ -40,6 +40,9 @@ export function DownloadManagerProvider({ children }: { children: React.ReactNod
}, [activeDownloads]) }, [activeDownloads])
useEffect(() => { useEffect(() => {
// 监听 Git 进度 // 监听 Git 进度
window.electronAPI.git.onProgress((data) => { window.electronAPI.git.onProgress((data) => {
const { repoUrl, raw } = data const { repoUrl, raw } = data
@ -86,7 +89,7 @@ export function DownloadManagerProvider({ children }: { children: React.ReactNod
} }
}, []) }, [])
const startDownload = async (repoUrl: string, targetDir: string) => { const startDownload = async (repoUrl: string, targetDir: string, branch?: string) => {
const id = Math.random().toString(36).substring(7) const id = Math.random().toString(36).substring(7)
const newItem: DownloadItem = { const newItem: DownloadItem = {
id, id,
@ -101,7 +104,8 @@ export function DownloadManagerProvider({ children }: { children: React.ReactNod
setActiveDownloads(prev => [...prev, newItem]) setActiveDownloads(prev => [...prev, newItem])
try { try {
const result = await window.electronAPI.git.clone(repoUrl, targetDir) // @ts-ignore - Assuming electronAPI is updated to accept branch
const result = await window.electronAPI.git.clone(repoUrl, targetDir, branch)
if (result.success) { if (result.success) {
setActiveDownloads(prev => prev.map(item => setActiveDownloads(prev => prev.map(item =>

View File

@ -2,9 +2,11 @@ import { Search, Plus, User, Minus, Square, X } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useToast } from '@/components/ui/toast-provider'
export function Header() { export function Header() {
const navigate = useNavigate() const navigate = useNavigate()
const { showToast } = useToast()
const handleMinimize = () => { const handleMinimize = () => {
window.electronAPI?.window.minimize() window.electronAPI?.window.minimize()
@ -23,10 +25,10 @@ export function Header() {
{/* Left: Logo */} {/* Left: Logo */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center"> <div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center">
{/* Simple Logo Icon Placeholder */} {/* Simple Logo Icon Placeholder */}
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
</div> </div>
<span className="text-xl font-bold text-slate-800"></span> <span className="text-xl font-bold text-slate-800"></span>
</div> </div>
@ -54,11 +56,36 @@ export function Header() {
<div className="h-6 w-px bg-slate-200 mx-2"></div> <div className="h-6 w-px bg-slate-200 mx-2"></div>
<div className="flex items-center gap-3"> <div className="relative group h-full flex items-center">
<div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center overflow-hidden border border-slate-200"> <div
<User className="w-4 h-4 text-slate-400" /> className="flex items-center gap-3 cursor-pointer hover:bg-slate-50 p-1 rounded-md transition-colors h-full"
onClick={() => navigate('/profile')}
>
<div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center overflow-hidden border border-slate-200">
<User className="w-4 h-4 text-slate-400" />
</div>
{/* Removed text as requested */}
</div>
{/* Dropdown Menu - Adjusted position and content */}
<div className="absolute right-0 top-full mt-1 w-32 bg-white border border-slate-200 rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right z-50">
<div className="py-1">
<button
onClick={(e) => {
e.stopPropagation();
localStorage.removeItem('token');
showToast('已安全退出', 'success');
navigate('/'); // Go to login page
}}
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
退
</button>
</div>
</div> </div>
<span className="text-sm font-medium text-slate-600">XXX</span>
</div> </div>
{/* Window Controls */} {/* Window Controls */}

View File

@ -41,4 +41,12 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
<ToastContainer toasts={toasts} onClose={removeToast} /> <ToastContainer toasts={toasts} onClose={removeToast} />
</ToastContext.Provider> </ToastContext.Provider>
) )
}
export function useToast() {
const context = React.useContext(ToastContext)
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider')
}
return context
} }

View File

@ -173,7 +173,7 @@ export const cacheUtils = {
*/ */
async has(key: string, config?: { namespace?: string }): Promise<boolean> { async has(key: string, config?: { namespace?: string }): Promise<boolean> {
const result = await window.electronAPI.cache.has(key, config) const result = await window.electronAPI.cache.has(key, config)
return result.success ? result.exists : false return result.success ? (result.exists ?? false) : false
}, },
/** /**

View File

@ -2,17 +2,21 @@ import { Outlet } from 'react-router-dom'
import { Sidebar } from '@/components/layout/Sidebar' import { Sidebar } from '@/components/layout/Sidebar'
import { Header } from '@/components/layout/Header' import { Header } from '@/components/layout/Header'
export default function MainLayout() { interface MainLayoutProps {
showSidebar?: boolean
}
export default function MainLayout({ showSidebar = true }: MainLayoutProps) {
return ( return (
<div className="h-screen w-full bg-slate-50 flex flex-col overflow-hidden"> <div className="h-screen w-full bg-slate-50 flex flex-col overflow-hidden">
{/* Header at the top */} {/* Header at the top */}
<Header /> <Header />
{/* Main content area below header */} {/* Main content area below header */}
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{/* Sidebar on the left */} {/* Sidebar on the left */}
<Sidebar /> {showSidebar && <Sidebar />}
{/* Page Content on the right */} {/* Page Content on the right */}
<main className="flex-1 overflow-auto p-6"> <main className="flex-1 overflow-auto p-6">
<Outlet /> <Outlet />

View File

@ -4,16 +4,43 @@ import { User, Lock, Power } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import bgImage from '@/assets/images/登录背景.png'
import { login } from '@/api/auth'
import { useToast } from '@/components/ui/toast-provider'
export default function Login() { export default function Login() {
const navigate = useNavigate() const navigate = useNavigate()
const [username, setUsername] = useState('test1') const { showToast } = useToast()
const [password, setPassword] = useState('') const [username, setUsername] = useState('admin') // Default to admin as per request
const [password, setPassword] = useState('123456') // Default password
const [loading, setLoading] = useState(false)
const handleLogin = () => { const handleLogin = async () => {
// 简单的登录模拟 if (!username || !password) {
console.log('Login with:', username, password) showToast('请输入用户名和密码', 'error')
navigate('/app') return
}
setLoading(true)
try {
const res = await login({ username, password })
// simClient returns response.data directly via interceptor
// res structure: { msg: "...", code: 200, data: ..., token: "..." }
if (res.code === 200) {
localStorage.setItem('token', res.token)
showToast('登录成功', 'success')
navigate('/app')
} else {
showToast(res.msg || '登录失败', 'error')
}
} catch (error: any) {
console.error('Login error:', error)
const errorMsg = error.response?.data?.msg || error.message || '登录请求失败';
showToast(errorMsg, 'error')
} finally {
setLoading(false)
}
} }
return ( return (
@ -21,16 +48,16 @@ export default function Login() {
{/* 顶部拖动区域 - 降低 z-index 以免覆盖交互元素 */} {/* 顶部拖动区域 - 降低 z-index 以免覆盖交互元素 */}
<div className="absolute top-0 left-0 right-0 h-12 drag-region z-40" /> <div className="absolute top-0 left-0 right-0 h-12 drag-region z-40" />
<div <div
className="absolute inset-0 z-0" className="absolute inset-0 z-0"
style={{ style={{
//background: 'linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%)', // 默认淡蓝色渐变 //background: 'linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%)', // 默认淡蓝色渐变
backgroundImage: 'url("/src/assets/images/登录背景.png")', // 替换为实际背景图路径 backgroundImage: `url("${bgImage}")`, // 使用导入的图片以确保打包后路径正确
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
}} }}
/> />
{/* 顶部退出登录按钮 */} {/* 顶部退出登录按钮 */}
<div className="absolute top-6 right-8 z-50 flex items-center gap-2 text-slate-600 hover:text-slate-900 cursor-pointer transition-colors no-drag"> <div className="absolute top-6 right-8 z-50 flex items-center gap-2 text-slate-600 hover:text-slate-900 cursor-pointer transition-colors no-drag">
<Power className="w-4 h-4" /> <Power className="w-4 h-4" />
@ -40,14 +67,14 @@ export default function Login() {
{/* 主要内容区域 */} {/* 主要内容区域 */}
<div className="relative z-10 flex min-h-screen items-center justify-center px-4 sm:px-6 lg:px-8"> <div className="relative z-10 flex min-h-screen items-center justify-center px-4 sm:px-6 lg:px-8">
<div className="flex w-full max-w-6xl items-center justify-between gap-16"> <div className="flex w-full max-w-6xl items-center justify-between gap-16">
{/* 左侧装饰区域 (对应图中的文件夹 3D 图标) */} {/* 左侧装饰区域 (对应图中的文件夹 3D 图标) */}
<div className="hidden lg:flex flex-1 items-center justify-center"> <div className="hidden lg:flex flex-1 items-center justify-center">
{/* {/*
TODO: 这里是放置左侧 3D TODO: 这里是放置左侧 3D
使 使
*/} */}
<div className="w-full h-full min-h-[400px]"></div> <div className="w-full h-full min-h-[400px]"></div>
</div> </div>
{/* 右侧登录卡片 */} {/* 右侧登录卡片 */}
@ -89,16 +116,24 @@ export default function Login() {
</div> </div>
{/* 登录按钮 */} {/* 登录按钮 */}
<Button <Button
className="w-full h-12 text-base bg-[#0e7490] hover:bg-[#155e75] transition-all duration-300 shadow-lg shadow-cyan-900/20" className="w-full h-12 text-base bg-[#0e7490] hover:bg-[#155e75] transition-all duration-300 shadow-lg shadow-cyan-900/20"
onClick={handleLogin} onClick={handleLogin}
disabled={loading}
> >
{loading ? '登 录 中...' : '登 录'}
</Button> </Button>
{/* 底部链接 */} {/* 底部链接 */}
<div className="text-center mt-6"> <div className="text-center mt-6">
<a href="#" className="text-sm text-[#0e7490] hover:underline font-medium"> <a
href="#"
onClick={(e) => {
e.preventDefault()
navigate('/app')
}}
className="text-sm text-[#0e7490] hover:underline font-medium"
>
线访 线访
</a> </a>
</div> </div>

264
src/pages/ProfilePage.tsx Normal file
View File

@ -0,0 +1,264 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
ArrowLeft,
User,
Box,
Heart,
Edit
} from 'lucide-react'
import { cn } from '@/lib/utils'
// Mock Data
const MOCK_RESOURCES = [
{
id: 1,
name: '大学物理实验室场景组件',
thumbnail: 'https://images.unsplash.com/photo-1614726365723-49cfaeb5d203?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
tags: ['Unity', '实验室场景'],
uploadTime: '2024-05-10',
downloads: 128,
status: 'published', // published, draft, auditing
},
{
id: 2,
name: '光学实验交互插件 V2.0',
thumbnail: 'https://images.unsplash.com/photo-1532094349884-543bc11b234d?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
tags: ['Unity', '交互插件'],
uploadTime: '2024-06-15',
downloads: 86,
status: 'published',
},
{
id: 3,
name: '力学虚拟实验项目模板',
thumbnail: 'https://images.unsplash.com/photo-1581093458791-9f3c3900df4b?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
tags: ['Unreal', '项目模板'],
uploadTime: '2024-07-02',
downloads: 0,
status: 'draft',
},
{
id: 4,
name: '电磁学数据采集工具',
thumbnail: 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
tags: ['Python', '数据工具'],
uploadTime: '2024-07-10',
downloads: 0,
status: 'auditing',
}
]
export default function ProfilePage() {
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState('resources') // info, resources, favorites
const [resourceFilter, setResourceFilter] = useState('all') // all, published, draft, auditing
const getStatusBadge = (status: string) => {
switch (status) {
case 'published':
return <span className="text-blue-500 bg-blue-50 px-2 py-0.5 rounded text-xs"></span>
case 'draft':
return <span className="text-slate-500 bg-slate-100 px-2 py-0.5 rounded text-xs">稿</span>
case 'auditing':
return <span className="text-amber-500 bg-amber-50 px-2 py-0.5 rounded text-xs"></span>
default:
return null
}
}
return (
<div className="flex flex-col h-full bg-slate-50">
<main className="flex-1 w-full p-2.5 flex flex-col gap-2.5 overflow-hidden">
{/* Back Button */}
<div className="shrink-0">
<button
onClick={() => navigate("/app")}
className="flex items-center gap-1 text-slate-500 hover:text-slate-800 text-sm font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4" />
</button>
</div>
<div className="flex-1 flex w-full p-2.5 gap-6 h-full">
{/* Left Sidebar */}
<div className="w-64 flex-shrink-0 space-y-6">
{/* User Profile Card */}
<div className="bg-white rounded-xl p-6 border border-slate-200 flex flex-col items-center">
<div className="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mb-4 relative group cursor-pointer overflow-hidden">
{/* Placeholder Avatar */}
<User className="w-8 h-8 text-slate-400" />
<div className="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center transition-all">
<Edit className="w-4 h-4 text-white" />
</div>
</div>
<div className="text-center mb-1">
<h2 className="font-bold text-lg text-slate-800 flex items-center gap-2 justify-center">
<span className="text-[10px] bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded border border-amber-200">
</span>
</h2>
</div>
<div className="flex items-center gap-4 text-xs text-slate-500 mt-2 w-full justify-center">
<div className="flex flex-col items-center">
<span className="font-bold text-slate-800 text-base">28</span>
<span></span>
</div>
<div className="w-px h-8 bg-slate-100"></div>
<div className="flex flex-col items-center">
<span className="font-bold text-slate-800 text-base">12</span>
<span></span>
</div>
</div>
</div>
{/* Navigation Menu */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="p-2 space-y-1">
<button
onClick={() => setActiveTab('info')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-lg transition-colors",
activeTab === 'info'
? "bg-cyan-50 text-[#0e7490]"
: "text-slate-600 hover:bg-slate-50"
)}
>
<User className="w-4 h-4" />
</button>
<button
onClick={() => setActiveTab('resources')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-lg transition-colors",
activeTab === 'resources'
? "bg-cyan-50 text-[#0e7490]"
: "text-slate-600 hover:bg-slate-50"
)}
>
<Box className="w-4 h-4" />
</button>
<button
onClick={() => setActiveTab('favorites')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-lg transition-colors",
activeTab === 'favorites'
? "bg-cyan-50 text-[#0e7490]"
: "text-slate-600 hover:bg-slate-50"
)}
>
<Heart className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Right Content Area */}
<div className="flex-1 bg-white rounded-xl border border-slate-200 min-h-[600px] flex flex-col">
{activeTab === 'resources' && (
<>
<div className="p-6 border-b border-slate-100">
<h1 className="text-xl font-bold text-slate-800 mb-6"></h1>
{/* Tabs */}
<div className="flex items-center gap-8 border-b border-slate-100">
{['all', 'published', 'draft', 'auditing'].map((tab) => (
<button
key={tab}
onClick={() => setResourceFilter(tab)}
className={cn(
"pb-3 text-sm font-medium transition-all relative",
resourceFilter === tab
? "text-[#0e7490]"
: "text-slate-500 hover:text-slate-800"
)}
>
{tab === 'all' && '全部资源'}
{tab === 'published' && '已发布'}
{tab === 'draft' && '草稿箱'}
{tab === 'auditing' && '审核中'}
{resourceFilter === tab && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#0e7490]"></div>
)}
</button>
))}
</div>
</div>
{/* Table List */}
<div className="p-6">
<table className="w-full text-left text-sm">
<thead>
<tr className="text-slate-400 border-b border-slate-100">
<th className="pb-4 font-normal w-24"></th>
<th className="pb-4 font-normal"></th>
<th className="pb-4 font-normal"></th>
<th className="pb-4 font-normal"></th>
<th className="pb-4 font-normal"></th>
<th className="pb-4 font-normal"></th>
<th className="pb-4 font-normal text-right"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{MOCK_RESOURCES.filter(item => resourceFilter === 'all' || item.status === resourceFilter).map((item) => (
<tr key={item.id} className="group hover:bg-slate-50">
<td className="py-4">
<div className="w-16 h-10 bg-slate-100 rounded overflow-hidden border border-slate-200">
<img src={item.thumbnail} alt="" className="w-full h-full object-cover" />
</div>
</td>
<td className="py-4 font-medium text-slate-700">{item.name}</td>
<td className="py-4">
<div className="flex gap-1.5">
{item.tags.map(tag => (
<span key={tag} className="px-1.5 py-0.5 bg-slate-100 text-slate-500 rounded text-xs">{tag}</span>
))}
</div>
</td>
<td className="py-4 text-slate-500">{item.uploadTime}</td>
<td className="py-4 text-slate-500">{item.downloads} </td>
<td className="py-4">{getStatusBadge(item.status)}</td>
<td className="py-4 text-right">
<div className="flex justify-end gap-3 text-xs">
<button className="text-[#0e7490] hover:underline"></button>
{item.status === 'published' && <button className="text-slate-500 hover:text-[#0e7490] hover:underline"></button>}
{item.status === 'auditing' && <button className="text-slate-500 hover:text-[#0e7490] hover:underline"></button>}
{item.status === 'draft' && <button className="text-[#0e7490] hover:underline"></button>}
<button className="text-red-500 hover:underline"></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{MOCK_RESOURCES.filter(item => resourceFilter === 'all' || item.status === resourceFilter).length === 0 && (
<div className="py-20 text-center text-slate-400">
</div>
)}
</div>
</>
)}
{/* Placeholders for other tabs */}
{activeTab !== 'resources' && (
<div className="flex items-center justify-center h-full text-slate-400">
...
</div>
)}
</div>
</div>
</main>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { useState } from 'react' import { useState, useEffect } from 'react'
import { Filter, Download, Eye, Heart } from 'lucide-react' import { Filter, Download, Eye, Heart } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardFooter } from '@/components/ui/card' import { Card, CardContent, CardFooter } from '@/components/ui/card'
@ -9,29 +9,41 @@ const tabs = [
"全部", "地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊" "全部", "地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"
] ]
interface Asset { import { getSimulationUploadList } from '@/api/simulation'
id: string import { SimulationResource } from '@/api/simulationTypes'
title: string import { Loader2 } from 'lucide-react'
tags: string[]
author: string
date: string
downloads: number
imageUrl: string // In a real app, this would be a real URL
}
const mockAssets: Asset[] = Array(10).fill(null).map((_, i) => ({
id: `asset-${i}`,
title: 'SKY MASTER ULTIMATE 天空大师终极版版版版版版版',
tags: ['Unity', '实验室场景'],
author: '张老师',
date: '2021-05-10',
downloads: 128,
// Using a placeholder image color or pattern
imageUrl: ''
}))
export default function AssetLibrary() { export default function AssetLibrary() {
const [activeTab, setActiveTab] = useState("全部") const [activeTab, setActiveTab] = useState("全部")
const [assets, setAssets] = useState<SimulationResource[]>([])
const [loading, setLoading] = useState(false)
const [total, setTotal] = useState(0)
// Fetch data
useEffect(() => {
const fetchAssets = async () => {
try {
setLoading(true)
const response = await getSimulationUploadList({
pageNum: 1,
pageSize: 20, // Fetch more initially
// If "全部" is selected, don't filter by type (or handle specific types if needed)
types: activeTab === "全部" ? undefined : activeTab
})
if (response.code === 200) {
setAssets(response.rows || [])
setTotal(response.total || 0)
}
} catch (error) {
console.error("Failed to fetch assets:", error)
} finally {
setLoading(false)
}
}
fetchAssets()
}, [activeTab])
return ( return (
<div className="bg-white rounded-xl shadow-sm min-h-full flex flex-col"> <div className="bg-white rounded-xl shadow-sm min-h-full flex flex-col">
@ -56,7 +68,7 @@ export default function AssetLibrary() {
</button> </button>
))} ))}
</div> </div>
<Button variant="ghost" size="sm" className="gap-2 text-slate-600 hover:text-[#0e7490]"> <Button variant="ghost" size="sm" className="gap-2 text-slate-600 hover:text-[#0e7490]">
<Filter className="w-4 h-4" /> <Filter className="w-4 h-4" />
@ -65,17 +77,25 @@ export default function AssetLibrary() {
{/* Asset Grid */} {/* Asset Grid */}
<div className="p-6"> <div className="p-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> {loading ? (
{mockAssets.map((asset) => ( <div className="flex h-64 items-center justify-center text-slate-400">
<Card key={asset.id} className="overflow-hidden hover:shadow-xl transition-all duration-300 border-slate-100 group bg-white"> <Loader2 className="w-8 h-8 animate-spin" />
{/* Image Area */} </div>
<div className="aspect-video bg-slate-100 relative overflow-hidden"> ) : assets.length === 0 ? (
{/* Placeholder Gradient */} <div className="flex h-64 items-center justify-center text-slate-400 text-sm">
<div className="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-300 group-hover:scale-105 transition-transform duration-500" />
{/* You would put <img src={asset.imageUrl} ... /> here */} </div>
) : (
{/* Hover Actions Overlay */} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div className="absolute top-3 right-3 flex gap-2 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-y-[-10px] group-hover:translate-y-0 z-10"> {assets.map((asset) => (
<Card key={asset.id} className="overflow-hidden hover:shadow-xl transition-all duration-300 border-slate-100 group bg-white">
{/* Image Area */}
<div className="aspect-video bg-slate-100 relative overflow-hidden">
{/* Placeholder Gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-300 group-hover:scale-105 transition-transform duration-500" />
{/* Hover Actions Overlay */}
<div className="absolute top-3 right-3 flex gap-2 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-y-[-10px] group-hover:translate-y-0 z-10">
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors"> <button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</button> </button>
@ -85,37 +105,38 @@ export default function AssetLibrary() {
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors"> <button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
</button> </button>
</div> </div>
</div>
<CardContent className="p-4 space-y-3">
<h3 className="font-bold text-slate-800 line-clamp-2 text-sm leading-relaxed min-h-[40px]" title={asset.title}>
{asset.title}
</h3>
<div className="flex flex-wrap gap-2">
{asset.tags.map(tag => (
<span key={tag} className="px-2 py-0.5 bg-slate-50 text-slate-400 text-xs rounded-sm border border-slate-100">
{tag}
</span>
))}
</div> </div>
</CardContent>
<CardContent className="p-4 space-y-3">
<CardFooter className="p-4 pt-0 flex items-center justify-between text-xs text-slate-400 border-t border-slate-50 mt-2 pt-3"> <h3 className="font-bold text-slate-800 line-clamp-2 text-sm leading-relaxed min-h-[40px]" title={asset.name}>
<div className="flex items-center gap-2"> {asset.name}
<span>{asset.author}</span> </h3>
<span className="text-slate-300">|</span>
<span>{asset.date}</span> <div className="flex flex-wrap gap-2">
</div> {asset.tag && asset.tag.split(/[,]/).map(tag => (
<div className="flex items-center gap-1"> <span key={tag} className="px-2 py-0.5 bg-slate-50 text-slate-400 text-xs rounded-sm border border-slate-100">
<Download className="w-3 h-3" /> {tag.trim()}
<span>{asset.downloads}</span> </span>
</div> ))}
</CardFooter> </div>
</Card> </CardContent>
))}
</div> <CardFooter className="p-4 pt-0 flex items-center justify-between text-xs text-slate-400 border-t border-slate-50 mt-2 pt-3">
<div className="flex items-center gap-2">
<span>{asset.createdBy || 'Unknown'}</span>
<span className="text-slate-300">|</span>
<span>{asset.createdTime ? asset.createdTime.split(' ')[0] : '-'}</span>
</div>
<div className="flex items-center gap-1">
<Download className="w-3 h-3" />
<span>0</span>
</div>
</CardFooter>
</Card>
))}
</div>
)}
</div> </div>
</div> </div>
) )

View File

@ -1,23 +1,43 @@
import { useState, type ChangeEvent } from 'react' import { useState, type ChangeEvent, useContext } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ArrowLeft, Power, Upload, X, FileText, CheckCircle2 } from 'lucide-react' import { ArrowLeft, Upload, X, CheckCircle2, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { addSimulationResource } from '@/api/simulation'
import { AddSimulationResourceRequest, ProUploadItem } from '@/api/simulationTypes'
import { ToastContext } from '@/components/ui/toast-provider'
const RESOURCE_LIBRARIES: Record<string, string[]> = {
'插件库': ["地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"],
'组件库': ["UI组件", "功能组件", "逻辑组件", "数据组件"],
'工具库': ["调试工具", "构建工具", "测试工具", "效率工具"],
'原型库': ["低保真", "高保真", "交互原型", "静态原型"],
'设计库': ["图标", "图片", "配色方案", "字体"],
'项目库': ["物理仿真", "化学仿真", "生物仿真", "工程仿真"],
'文件库': ["文档", "表格", "幻灯片", "PDF", "图片", "视频"],
'案例库': ["教学案例", "实训案例", "演示案例"]
}
export default function UploadResource() { export default function UploadResource() {
const navigate = useNavigate() const navigate = useNavigate()
const toast = useContext(ToastContext)
const [currentStep, setCurrentStep] = useState(1) const [currentStep, setCurrentStep] = useState(1)
// 表单状态 // 表单状态
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '12', name: '虚拟仿真实验平台',
type: 'Unity插件/场景', stores: '插件库',
category: '天气', type: '地形',
tags: ['Unity', 'Unreal', '3D模型'], category: '教学平台',
description: '', tags: ['仿真', '教学', '3D'],
detailDescription: '', description: '用于物理化学实验的虚拟仿真平台',
detailDescription: '该平台支持多种实验场景,包括光学、力学、化学等实验模块',
urls: 'http://example.com',
versions: 'v1.0.0',
env: 'Windows 10/Linux',
createdBy: 'Admin',
files: [] as File[], files: [] as File[],
}) })
@ -46,50 +66,73 @@ export default function UploadResource() {
setIsUploading(true) setIsUploading(true)
let progress = 0 let progress = 0
const interval = setInterval(() => { const interval = setInterval(() => {
progress += 10 progress += 20
setUploadProgress(progress) setUploadProgress(progress)
if (progress >= 100) { if (progress >= 100) {
clearInterval(interval) clearInterval(interval)
setIsUploading(false) setIsUploading(false)
} }
}, 200) }, 100)
setFormData({ ...formData, files: Array.from(e.target.files) }) setFormData({ ...formData, files: Array.from(e.target.files) })
} }
} }
return ( const handleSubmit = async () => {
<div className="h-screen bg-slate-50 flex flex-col overflow-hidden"> try {
{/* Custom Header for Upload Page */} setIsUploading(true)
<header className="h-16 bg-white border-b px-6 flex items-center justify-between shadow-sm shrink-0">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-xl font-bold text-slate-800"></span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-slate-600">
<div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center">
<UserIcon />
</div>
<span className="text-sm font-medium">XXX</span>
</div>
<div className="h-4 w-[1px] bg-slate-300"></div>
<button className="p-2 hover:bg-slate-100 rounded-full text-slate-500 transition-colors">
<Power className="w-5 h-5" />
</button>
</div>
</header>
const proUploadList: ProUploadItem[] = formData.files.map((file: File) => ({
sourceName: formData.name,
sourceForm: "RES-" + Date.now(),
fileType: file.name.split('.').pop()?.toUpperCase() || 'OTHER',
fileName: file.name,
fileUrl: "/upload/temp/" + file.name, // Mock URL
fileVersion: formData.versions
}))
const requestData: AddSimulationResourceRequest = {
name: formData.name,
stores: formData.stores,
types: formData.type,
category: formData.category,
tag: formData.tags.join(','),
introduction: formData.description,
details: formData.detailDescription,
urls: formData.urls,
versions: formData.versions,
env: formData.env,
createdBy: formData.createdBy,
proUploadList
}
const response = await addSimulationResource(requestData)
if (response.code === 200 || response.code === 0) {
toast?.showToast('上传成功!', 'success')
navigate('/app')
} else {
toast?.showToast(response.msg || '上传失败,请稍后重试', 'error')
}
} catch (error) {
console.error('Submit error:', error)
toast?.showToast('请求出错,请检查网络连接', 'error')
} finally {
setIsUploading(false)
}
}
return (
<div className="flex flex-col h-full bg-slate-50">
{/* Main Content */}
{/* Main Content */} {/* Main Content */}
<main className="flex-1 w-full p-2.5 flex flex-col gap-2.5 overflow-hidden"> <main className="flex-1 w-full p-2.5 flex flex-col gap-2.5 overflow-hidden">
{/* Back Button */} {/* Back Button */}
<div className="shrink-0"> <div className="shrink-0">
<button <button
onClick={() => navigate(-1)} onClick={() => navigate('/app')}
className="flex items-center gap-1 text-slate-500 hover:text-slate-800 text-sm font-medium transition-colors" className="flex items-center gap-1 text-slate-500 hover:text-slate-800 text-sm font-medium transition-colors"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
@ -109,7 +152,7 @@ export default function UploadResource() {
<div className="absolute inset-0 bg-slate-200 border-t border-dashed border-slate-300" /> <div className="absolute inset-0 bg-slate-200 border-t border-dashed border-slate-300" />
{/* Active Connection Line - 已完成的连接线 */} {/* Active Connection Line - 已完成的连接线 */}
{currentStep > 1 && ( {currentStep > 1 && (
<div <div
className="absolute left-0 top-0 h-[1px] bg-[#0e7490] transition-all duration-300" className="absolute left-0 top-0 h-[1px] bg-[#0e7490] transition-all duration-300"
style={{ style={{
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`, width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
@ -117,15 +160,15 @@ export default function UploadResource() {
/> />
)} )}
</div> </div>
{/* Step Circles and Labels - 使用 justify-between 让第一个和最后一个分别在两端 */} {/* Step Circles and Labels - 使用 justify-between 让第一个和最后一个分别在两端 */}
<div className="flex justify-between items-start relative z-10"> <div className="flex justify-between items-start relative z-10">
{steps.map((step, index) => ( {steps.map((step) => (
<div key={step.number} className="flex flex-col items-center flex-shrink-0"> <div key={step.number} className="flex flex-col items-center flex-shrink-0">
<div className={cn( <div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-colors duration-300 mb-2 relative z-10", "w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-colors duration-300 mb-2 relative z-10",
currentStep >= step.number currentStep >= step.number
? "bg-[#0e7490] text-white" ? "bg-[#0e7490] text-white"
: "bg-slate-100 text-slate-400" : "bg-slate-100 text-slate-400"
)}> )}>
{currentStep > step.number ? <CheckCircle2 className="w-5 h-5" /> : step.number} {currentStep > step.number ? <CheckCircle2 className="w-5 h-5" /> : step.number}
@ -145,29 +188,54 @@ export default function UploadResource() {
{currentStep === 1 && ( {currentStep === 1 && (
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300"> <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
<h3 className="text-base font-bold text-slate-800 mb-6"></h3> <h3 className="text-base font-bold text-slate-800 mb-6"></h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<Input <Input
value={formData.name} value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})} onChange={e => setFormData({ ...formData, name: e.target.value })}
/> />
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={formData.stores}
onChange={e => {
const newStore = e.target.value;
setFormData({
...formData,
stores: newStore,
type: RESOURCE_LIBRARIES[newStore]?.[0] || ''
})
}}
>
{Object.keys(RESOURCE_LIBRARIES).map(store => (
<option key={store} value={store}>{store}</option>
))}
</select>
</div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<Input <select
value={formData.type} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
onChange={e => setFormData({...formData, type: e.target.value})} value={formData.type}
/> onChange={e => setFormData({ ...formData, type: e.target.value })}
>
{RESOURCE_LIBRARIES[formData.stores]?.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<Input <Input
value={formData.category} value={formData.category}
onChange={e => setFormData({...formData, category: e.target.value})} onChange={e => setFormData({ ...formData, category: e.target.value })}
/> />
</div> </div>
@ -181,7 +249,7 @@ export default function UploadResource() {
<button className="hover:text-cyan-200"><X className="w-3 h-3" /></button> <button className="hover:text-cyan-200"><X className="w-3 h-3" /></button>
</span> </span>
))} ))}
{['交互插件', '渲染效果', '数据采集', '虚拟实验室', '教学模板'].map(tag => ( {['交互插件', '渲染效果', '数据采集', '虚拟实验室', '教学模板'].map((tag: string) => (
<button key={tag} className="bg-slate-100 text-slate-600 hover:bg-slate-200 px-3 py-1 rounded-full text-xs transition-colors"> <button key={tag} className="bg-slate-100 text-slate-600 hover:bg-slate-200 px-3 py-1 rounded-full text-xs transition-colors">
{tag} {tag}
</button> </button>
@ -193,8 +261,8 @@ export default function UploadResource() {
</div> </div>
<div className="flex justify-between pt-10"> <div className="flex justify-between pt-10">
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button> <Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button>
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button> <Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button>
</div> </div>
</div> </div>
)} )}
@ -207,9 +275,9 @@ export default function UploadResource() {
{/* File Drop Zone */} {/* File Drop Zone */}
<div className="border-2 border-dashed border-slate-200 rounded-lg p-10 flex flex-col items-center justify-center text-center hover:bg-slate-50 transition-colors cursor-pointer relative"> <div className="border-2 border-dashed border-slate-200 rounded-lg p-10 flex flex-col items-center justify-center text-center hover:bg-slate-50 transition-colors cursor-pointer relative">
<input <input
type="file" type="file"
className="absolute inset-0 opacity-0 cursor-pointer" className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileChange} onChange={handleFileChange}
/> />
<div className="w-12 h-12 bg-sky-100 text-sky-500 rounded-lg flex items-center justify-center mb-4"> <div className="w-12 h-12 bg-sky-100 text-sky-500 rounded-lg flex items-center justify-center mb-4">
@ -221,47 +289,47 @@ export default function UploadResource() {
{/* Progress */} {/* Progress */}
{(isUploading || formData.files.length > 0) && ( {(isUploading || formData.files.length > 0) && (
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-4"> <div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-4">
<div className="w-10 h-10 bg-red-100 rounded flex items-center justify-center text-red-500 font-bold text-xs"> <div className="w-10 h-10 bg-red-100 rounded flex items-center justify-center text-red-500 font-bold text-xs">
PDF PDF
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex justify-between text-xs mb-1"> <div className="flex justify-between text-xs mb-1">
<span className="font-medium text-slate-700">.pdf</span> <span className="font-medium text-slate-700">.pdf</span>
<span className="text-slate-400">34.01KB</span> <span className="text-slate-400">34.01KB</span>
</div> </div>
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden"> <div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-[#0e7490] transition-all duration-300" className="h-full bg-[#0e7490] transition-all duration-300"
style={{ width: `${uploadProgress}%` }} style={{ width: `${uploadProgress}%` }}
/> />
</div> </div>
</div> </div>
<button className="text-slate-400 hover:text-slate-600"> <button className="text-slate-400 hover:text-slate-600">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
)} )}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-800 text-xs text-slate-500"></h3> <h3 className="text-base font-bold text-slate-800 text-xs text-slate-500"></h3>
<div className="border-2 border-dashed border-slate-200 rounded-lg p-8 flex flex-col items-center justify-center text-center hover:bg-slate-50 transition-colors cursor-pointer h-40"> <div className="border-2 border-dashed border-slate-200 rounded-lg p-8 flex flex-col items-center justify-center text-center hover:bg-slate-50 transition-colors cursor-pointer h-40">
<div className="w-10 h-10 bg-slate-100 text-slate-400 rounded-lg flex items-center justify-center mb-2"> <div className="w-10 h-10 bg-slate-100 text-slate-400 rounded-lg flex items-center justify-center mb-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<p className="text-xs font-medium text-slate-600 mb-1"></p> <p className="text-xs font-medium text-slate-600 mb-1"></p>
<p className="text-[10px] text-slate-400">JPG/PNG | 800x600px | 5MB</p> <p className="text-[10px] text-slate-400">JPG/PNG | 800x600px | 5MB</p>
</div> </div>
</div> </div>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button> <Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" onClick={handlePrev}></Button> <Button variant="outline" onClick={handlePrev}></Button>
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button> <Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button>
</div> </div>
</div> </div>
</div> </div>
)} )}
@ -269,57 +337,69 @@ export default function UploadResource() {
{currentStep === 3 && ( {currentStep === 3 && (
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300"> <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
<h3 className="text-base font-bold text-slate-800 mb-6"></h3> <h3 className="text-base font-bold text-slate-800 mb-6"></h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-[100px_1fr] gap-4"> <div className="grid grid-cols-[100px_1fr] gap-4">
<label className="text-right text-sm text-slate-500 pt-2"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500 pt-2"><span className="text-red-500 mr-1">*</span></label>
<Textarea <Textarea
placeholder="请简要描述资源的功能、特点和使用场景200字以内" placeholder="请简要描述资源的功能、特点和使用场景200字以内"
className="min-h-[100px]" className="min-h-[100px]"
value={formData.description} value={formData.description}
onChange={e => setFormData({...formData, description: e.target.value})} onChange={e => setFormData({ ...formData, description: e.target.value })}
/> />
</div> </div>
<div className="grid grid-cols-[100px_1fr] gap-4"> <div className="grid grid-cols-[100px_1fr] gap-4">
<label className="text-right text-sm text-slate-500 pt-2"></label> <label className="text-right text-sm text-slate-500 pt-2"></label>
<Textarea <Textarea
placeholder="请详细描述资源的安装、使用、适配环境等信息(可选)" placeholder="请详细描述资源的安装、使用、适配环境等信息(可选)"
className="min-h-[150px]" className="min-h-[150px]"
value={formData.detailDescription} value={formData.detailDescription}
onChange={e => setFormData({...formData, detailDescription: e.target.value})} onChange={e => setFormData({ ...formData, detailDescription: e.target.value })}
/> />
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500">使/</label> <label className="text-right text-sm text-slate-500">使/</label>
<Input placeholder="如GitHub、百度网盘、在线文档等链接可选" /> <Input
placeholder="如GitHub、百度网盘、在线文档等链接可选"
value={formData.urls}
onChange={e => setFormData({ ...formData, urls: e.target.value })}
/>
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"></label> <label className="text-right text-sm text-slate-500"></label>
<Input placeholder="如V1.0、2021版可选" defaultValue="如V1.0、2021版可选" /> <Input
placeholder="如V1.0.0"
value={formData.versions}
onChange={e => setFormData({ ...formData, versions: e.target.value })}
/>
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"></label> <label className="text-right text-sm text-slate-500"></label>
<Input placeholder="如Unity2021.3、Windows 10可选" defaultValue="如Unity2021.3、Windows 10可选" /> <Input
placeholder="如Windows 10/Linux"
value={formData.env}
onChange={e => setFormData({ ...formData, env: e.target.value })}
/>
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-800 font-bold"></label> <label className="text-right text-sm text-slate-800 font-bold"></label>
<div className="bg-slate-50 p-3 rounded text-sm text-[#0e7490] font-medium"> <div className="bg-slate-50 p-3 rounded text-sm text-[#0e7490] font-medium">
.pdf .pdf
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-between pt-10"> <div className="flex justify-between pt-10">
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button> <Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" onClick={handlePrev}></Button> <Button variant="outline" onClick={handlePrev}></Button>
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button> <Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button>
</div> </div>
</div> </div>
</div> </div>
)} )}
@ -327,14 +407,18 @@ export default function UploadResource() {
{currentStep === 4 && ( {currentStep === 4 && (
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-300"> <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-300">
<h3 className="text-base font-bold text-slate-800"></h3> <h3 className="text-base font-bold text-slate-800"></h3>
<div className="bg-slate-50 rounded-lg p-6 space-y-4 text-sm"> <div className="bg-slate-50 rounded-lg p-6 space-y-4 text-sm">
<h4 className="font-bold text-slate-800 mb-4"></h4> <h4 className="font-bold text-slate-800 mb-4"></h4>
<div className="grid grid-cols-[80px_1fr] gap-4"> <div className="grid grid-cols-[80px_1fr] gap-4">
<span className="text-slate-500"></span> <span className="text-slate-500"></span>
<span className="text-slate-900 font-medium">{formData.name}</span> <span className="text-slate-900 font-medium">{formData.name}</span>
</div> </div>
<div className="grid grid-cols-[80px_1fr] gap-4">
<span className="text-slate-500"></span>
<span className="text-slate-900">{formData.stores}</span>
</div>
<div className="grid grid-cols-[80px_1fr] gap-4"> <div className="grid grid-cols-[80px_1fr] gap-4">
<span className="text-slate-500"></span> <span className="text-slate-500"></span>
<span className="text-slate-900">{formData.type}</span> <span className="text-slate-900">{formData.type}</span>
@ -358,31 +442,38 @@ export default function UploadResource() {
</div> </div>
<div className="bg-orange-50 border border-orange-100 rounded-lg p-4 text-sm text-orange-800"> <div className="bg-orange-50 border border-orange-100 rounded-lg p-4 text-sm text-orange-800">
<div className="flex items-center gap-2 font-bold mb-2"> <div className="flex items-center gap-2 font-bold mb-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg>
</div> </div>
<ul className="list-disc list-inside space-y-1 text-orange-700/80 text-xs ml-1"> <ul className="list-disc list-inside space-y-1 text-orange-700/80 text-xs ml-1">
<li></li> <li></li>
<li></li> <li></li>
<li>1-3/</li> <li>1-3/</li>
<li></li> <li></li>
</ul> </ul>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input type="checkbox" id="agreement" className="rounded border-slate-300 text-[#0e7490] focus:ring-[#0e7490]" /> <input type="checkbox" id="agreement" className="rounded border-slate-300 text-[#0e7490] focus:ring-[#0e7490]" />
<label htmlFor="agreement" className="text-sm text-slate-600"> <label htmlFor="agreement" className="text-sm text-slate-600">
<a href="#" className="text-[#0e7490] underline">仿</a> <a href="#" className="text-[#0e7490] underline"></a> <a href="#" className="text-[#0e7490] underline">仿</a> <a href="#" className="text-[#0e7490] underline"></a>
</label> </label>
</div> </div>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button> <Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">稿</Button>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" onClick={handlePrev}></Button> <Button variant="outline" onClick={handlePrev} disabled={isUploading}></Button>
<Button className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"></Button> <Button
</div> onClick={handleSubmit}
className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]"
disabled={isUploading}
>
{isUploading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
</Button>
</div>
</div> </div>
</div> </div>
)} )}
@ -393,10 +484,4 @@ export default function UploadResource() {
) )
} }
function UserIcon() {
return (
<svg className="w-5 h-5 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
</svg>
)
}

View File

@ -0,0 +1,329 @@
import { useState } from 'react'
import {
Folder,
FileText,
ChevronRight,
ChevronDown,
Filter,
Download,
FileEdit,
RefreshCw,
Plus,
MoreHorizontal,
LayoutTemplate,
History,
FileDown,
ExternalLink
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
// Mock Data for Directory Tree
const directoryData = [
{
id: '1',
name: '虚拟仿真课程文档库',
type: 'root',
tag: '文档库',
children: [
{
id: '1-1',
name: '物理仿真项目文档',
type: 'folder',
tag: '公共',
children: [
{
id: '1-1-1',
name: '光学实验仿真文档',
type: 'doc',
tag: '文档',
selected: true
},
{
id: '1-1-2',
name: '力学虚拟实验文档',
type: 'doc',
tag: '文档'
},
{
id: '1-1-3',
name: '电磁学仿真系统文档',
type: 'doc',
tag: '私有'
}
]
},
{
id: '1-2',
name: '化学仿真项目文档',
type: 'folder',
tag: '公共'
},
{
id: '1-3',
name: '文档模版库',
type: 'folder',
tag: '模版'
}
]
}
]
// Mock Data for Files Table
const associatedFiles = [
{ name: '光学实验原理说明.pdf', type: 'PDF', size: '1.2MB', date: '2024-07-20 14:30' },
{ name: '系统安装指南.docx', type: 'Word', size: '850KB', date: '2024-07-19 10:15' },
{ name: '常见问题汇编.xlsx', type: 'Excel', size: '420KB', date: '2024-07-18 16:45' },
]
export default function FileLibrary() {
const [expandedNodes, setExpandedNodes] = useState<string[]>(['1', '1-1'])
const toggleNode = (id: string) => {
setExpandedNodes(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
)
}
const renderTree = (nodes: any[]) => {
return nodes.map(node => (
<div key={node.id} className="ml-4">
<div
className={`flex items-center justify-between py-1.5 px-2 rounded-md cursor-pointer transition-colors ${node.selected ? 'bg-cyan-50 text-cyan-700' : 'hover:bg-slate-100 text-slate-600'}`}
onClick={() => node.children && toggleNode(node.id)}
>
<div className="flex items-center gap-2 overflow-hidden">
{node.children ? (
expandedNodes.includes(node.id) ? <ChevronDown className="w-4 h-4 shrink-0" /> : <ChevronRight className="w-4 h-4 shrink-0" />
) : (
<div className="w-4" />
)}
<div className="shrink-0">
{node.type === 'root' || node.type === 'folder' ? <Folder className={`w-4 h-4 ${node.type === 'root' ? 'text-blue-500' : 'text-amber-500'}`} /> : <FileText className="w-4 h-4 text-slate-400" />}
</div>
<span className="text-sm truncate font-medium">{node.name}</span>
</div>
{node.tag && (
<span className={`text-[10px] px-1.5 py-0.5 rounded shrink-0 ml-2 ${node.tag === '文档库' ? 'bg-blue-100 text-blue-600' :
node.tag === '公共' ? 'bg-green-100 text-green-600' :
node.tag === '文档' ? 'bg-sky-100 text-sky-600' :
node.tag === '私有' ? 'bg-orange-100 text-orange-600' :
'bg-purple-100 text-purple-600'
}`}>
{node.tag}
</span>
)}
</div>
{node.children && expandedNodes.includes(node.id) && (
<div className="mt-1">
{renderTree(node.children)}
</div>
)}
</div>
))
}
return (
<div className="flex h-full gap-6 overflow-hidden">
{/* Left Sidebar: Directory Tree */}
<div className="w-80 flex flex-col bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800 flex items-center gap-2">
</h2>
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400">
<Filter className="w-4 h-4" />
<span className="sr-only"></span>
</Button>
</div>
<div className="flex-1 overflow-auto p-2">
{/* Quick Use Templates Section */}
<div className="bg-slate-50/50 rounded-lg p-3 mb-4">
<div className="flex items-center gap-2 text-xs font-semibold text-slate-500 mb-3 ml-1">
<LayoutTemplate className="w-3.5 h-3.5" />
使
</div>
<div className="grid grid-cols-2 gap-2">
{['项目计划书', '需求规格说明书', '用户操作手册', '技术文档', '测试报告', '课程教案'].map((name) => (
<button
key={name}
className="bg-white border border-slate-200 rounded-md py-1.5 px-2 text-[11px] text-slate-600 text-left hover:border-cyan-500 hover:text-cyan-600 transition-colors shadow-sm flex items-center gap-1.5"
>
<FileEdit className="w-3 h-3 text-slate-300" />
<span className="truncate">{name}</span>
</button>
))}
</div>
</div>
{/* Recursive Tree */}
<div className="-ml-2">
{renderTree(directoryData)}
</div>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col gap-6 overflow-hidden">
{/* Top Header/Breadcrumbs and Actions */}
<div className="flex items-center justify-between bg-white rounded-xl border border-slate-200 px-6 py-3 shadow-sm">
<div className="flex items-center gap-1 text-sm font-medium text-slate-600">
<span>仿</span>
<ChevronRight className="w-4 h-4 text-slate-300" />
<span className="text-slate-900">仿</span>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" size="sm" className="gap-2 text-xs h-9">
<Download className="w-3.5 h-3.5 text-slate-500" />
</Button>
<Button variant="outline" size="sm" className="gap-2 text-xs h-9">
<LayoutTemplate className="w-3.5 h-3.5 text-slate-500" />
使
</Button>
<Button variant="ghost" size="sm" className="gap-2 text-xs h-9 text-slate-500">
<RefreshCw className="w-3.5 h-3.5" />
</Button>
</div>
</div>
{/* Document Preview Card */}
<Card className="flex-1 overflow-auto border-slate-200 shadow-sm flex flex-col">
<CardHeader className="py-4 border-b border-slate-100 flex-row items-center justify-between space-y-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center">
<FileText className="w-6 h-6" />
</div>
<div>
<CardTitle className="text-lg font-bold text-slate-800">仿.md</CardTitle>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors">
<ExternalLink className="w-3.5 h-3.5" />
</button>
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors">
<History className="w-3.5 h-3.5" />
</button>
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors">
<FileDown className="w-3.5 h-3.5" />
PDF
</button>
<button className="text-cyan-600 hover:text-cyan-700 font-bold flex items-center gap-1.5 transition-colors pl-4 border-l border-slate-200">
<FileEdit className="w-3.5 h-3.5" />
</button>
</div>
</CardHeader>
<CardContent className="p-8 prose prose-slate max-w-none prose-headings:text-slate-800 prose-strong:text-slate-900 prose-p:text-slate-600 text-slate-700">
<h1 className="text-3xl font-bold mb-4">仿</h1>
<div className="flex gap-6 text-sm text-slate-500 mb-8 pb-8 border-b border-slate-100">
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-700">:</span> V1.0
</div>
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-700">:</span> 2024-07-20
</div>
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-700">:</span>
</div>
</div>
<section className="space-y-6">
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">1.</h2>
<p>仿Unity引擎开发的虚拟仿真教学平台线</p>
</div>
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">2.</h2>
<ul className="list-disc pl-5 space-y-2">
<li><strong>:</strong> CPU i5及以上 8GB及以上 GTX1050及以上</li>
<li><strong>:</strong> Windows 10/11 64Unity Runtime 2021.3</li>
<li><strong>:</strong> 1920x1080 </li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">3.</h2>
<div className="space-y-3">
<p>3.1 启动系统:双击桌面快捷方式仿.exe</p>
<p>3.2 选择实验:在主界面左侧实验列表中选择需要进行的光学实验类型</p>
<p>3.3 :
<span className="block pl-4 mt-2 mb-2"> 使</span>
<span className="block pl-4 mb-2"> </span>
<span className="block pl-4 mb-2"> 仿</span>
<span className="block pl-4 mb-2"> </span>
</p>
</div>
</div>
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">4.</h2>
<p></p>
</div>
</section>
</CardContent>
</Card>
{/* Bottom Associated Files Table */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<div className="px-5 py-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-bold text-slate-800">
<Folder className="w-4 h-4 text-amber-500 fill-amber-500" />
</div>
<Button variant="ghost" size="sm" className="h-8 text-cyan-600 font-medium hover:bg-cyan-50">
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-slate-100 bg-white/50">
<th className="px-6 py-4 font-semibold text-slate-500"></th>
<th className="px-6 py-4 font-semibold text-slate-500"></th>
<th className="px-6 py-4 font-semibold text-slate-500"></th>
<th className="px-6 py-4 font-semibold text-slate-500"></th>
<th className="px-6 py-4 font-semibold text-slate-500 text-center"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{associatedFiles.map((file, idx) => (
<tr key={idx} className="hover:bg-slate-50/80 transition-colors group">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-slate-400 group-hover:bg-cyan-100 group-hover:text-cyan-600 transition-colors">
<FileText className="w-4 h-4" />
</div>
<span className="font-medium text-slate-700">{file.name}</span>
</div>
</td>
<td className="px-6 py-4 text-slate-500">{file.type}</td>
<td className="px-6 py-4 text-slate-500">{file.size}</td>
<td className="px-6 py-4 text-slate-500">{file.date}</td>
<td className="px-6 py-4">
<div className="flex items-center justify-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-[#0e7490] hover:bg-cyan-50">
<Download className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-slate-600">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ArrowLeft, Power } from 'lucide-react' import { ArrowLeft } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
@ -93,30 +93,7 @@ export default function NewProject() {
} }
return ( return (
<div className="h-screen bg-slate-50 flex flex-col overflow-hidden"> <div className="flex flex-col h-full bg-slate-50">
{/* Custom Header matching UploadResource */}
<header className="h-16 bg-white border-b px-6 flex items-center justify-between shadow-sm shrink-0">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-xl font-bold text-slate-800"></span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-slate-600">
<div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center">
<UserIcon />
</div>
<span className="text-sm font-medium">XXX</span>
</div>
<div className="h-4 w-[1px] bg-slate-300"></div>
<button className="p-2 hover:bg-slate-100 rounded-full text-slate-500 transition-colors">
<Power className="w-5 h-5" />
</button>
</div>
</header>
{/* Main Content */} {/* Main Content */}
<main className="flex-1 w-full p-2.5 flex flex-col gap-2.5 overflow-hidden"> <main className="flex-1 w-full p-2.5 flex flex-col gap-2.5 overflow-hidden">
@ -338,10 +315,4 @@ export default function NewProject() {
) )
} }
function UserIcon() {
return (
<svg className="w-5 h-5 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
</svg>
)
}

View File

@ -14,7 +14,10 @@ import {
ArrowDownToLine, ArrowDownToLine,
GitCommitHorizontal, GitCommitHorizontal,
User, User,
Calendar Calendar,
GitBranch,
ChevronsUpDown,
Check
} from 'lucide-react' } from 'lucide-react'
@ -107,6 +110,11 @@ export default function ProjectLibrary() {
const [commits, setCommits] = useState<GiteaCommit[]>([]) // Git Commits const [commits, setCommits] = useState<GiteaCommit[]>([]) // Git Commits
const [commitsLoading, setCommitsLoading] = useState(false) // Commits loading state const [commitsLoading, setCommitsLoading] = useState(false) // Commits loading state
// Branch Management
const [branches, setBranches] = useState<any[]>([])
const [selectedBranch, setSelectedBranch] = useState<string>('')
const [isBranchDropdownOpen, setIsBranchDropdownOpen] = useState(false)
// 当repos数据变化时构建树形结构 // 当repos数据变化时构建树形结构
useEffect(() => { useEffect(() => {
if (!repos || repos.length === 0) return if (!repos || repos.length === 0) return
@ -209,7 +217,30 @@ export default function ProjectLibrary() {
return return
} }
console.log(`调用API获取提交记录: owner=${selectedRepo.owner.username}, repo=${selectedRepo.name}`) console.log('调用API获取提交记录')
// 1. 首先尝试获取分支信息以快速填充 Banner通常比获取完整提交列表快得多
try {
const branchData = await giteaApi.getRepoBranch(selectedRepo.owner.username, selectedRepo.name, selectedRepo.default_branch)
if (branchData && branchData.commit) {
// 构造一个简单的 GiteaCommit 对象用于 Banner 显示
const latestCommit: any = {
sha: branchData.commit.id,
commit: {
message: branchData.commit.message,
author: {
name: branchData.commit.author.name,
date: branchData.commit.timestamp
}
}
}
setCommits([latestCommit])
// 这里不设置 Loading 结束,因为还要拉取列表
}
} catch (branchError) {
console.error("Failed to fetch branch info:", branchError)
}
// 2. 拉取完整的提交列表用于下方的记录显示
const data = await giteaApi.getRepoCommits(selectedRepo.owner.username, selectedRepo.name, 1, 20) const data = await giteaApi.getRepoCommits(selectedRepo.owner.username, selectedRepo.name, 1, 20)
setCommits(data) setCommits(data)
@ -229,6 +260,30 @@ export default function ProjectLibrary() {
} }
fetchCommits() fetchCommits()
fetchCommits()
}, [selectedRepo])
// Fetch Branches when Repo changes
useEffect(() => {
if (!selectedRepo) {
setBranches([])
setSelectedBranch('')
return
}
const fetchBranches = async () => {
try {
const data = await giteaApi.getRepoBranches(selectedRepo.owner.username, selectedRepo.name)
setBranches(data)
// Set default branch initially
if (!selectedBranch || selectedBranch !== selectedRepo.default_branch) {
setSelectedBranch(selectedRepo.default_branch)
}
} catch (error) {
console.error("Failed to fetch branches:", error)
}
}
fetchBranches()
}, [selectedRepo]) }, [selectedRepo])
/** /**
@ -517,7 +572,7 @@ export default function ProjectLibrary() {
const targetPath = result.path const targetPath = result.path
// 2. 委托给 DownloadManager 开始下载(支持排队和进度显示) // 2. 委托给 DownloadManager 开始下载(支持排队和进度显示)
startDownload(selectedRepo.clone_url, targetPath) startDownload(selectedRepo.clone_url, targetPath, selectedBranch)
} catch (error) { } catch (error) {
console.error('打开下载目录失败:', error) console.error('打开下载目录失败:', error)
@ -759,9 +814,49 @@ export default function ProjectLibrary() {
<div className="bg-slate-50/80 rounded-sm p-6 grid grid-cols-3 gap-8 text-sm shrink-0 border-0"> <div className="bg-slate-50/80 rounded-sm p-6 grid grid-cols-3 gap-8 text-sm shrink-0 border-0">
<div className="space-y-2"> <div className="space-y-2">
<div className="text-slate-400 text-xs"></div> <div className="text-slate-400 text-xs flex items-center gap-1">
<div className="text-slate-700 font-medium"> <GitBranch className="w-3 h-3" />
{selectedRepo.default_branch}(: {new Date(selectedRepo.updated_at).toLocaleDateString()})
</div>
<div className="relative">
<div
className="flex items-center gap-2 cursor-pointer hover:bg-slate-200/50 rounded p-1 -ml-1 transition-colors"
onClick={() => setIsBranchDropdownOpen(!isBranchDropdownOpen)}
>
<span className="text-slate-700 font-medium">{selectedBranch || 'Loading...'}</span>
<ChevronsUpDown className="w-3 h-3 text-slate-400" />
</div>
{/* Branch Dropdown */}
{isBranchDropdownOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setIsBranchDropdownOpen(false)}
/>
<div className="absolute top-full left-0 mt-1 w-56 max-h-60 overflow-y-auto bg-white border border-slate-200 rounded-md shadow-lg z-20 py-1">
{branches.map((branch) => (
<div
key={branch.name}
className={cn(
"px-3 py-2 text-sm flex items-center justify-between cursor-pointer hover:bg-slate-50",
selectedBranch === branch.name && "bg-cyan-50 text-[#0e7490]"
)}
onClick={() => {
setSelectedBranch(branch.name)
setIsBranchDropdownOpen(false)
}}
>
<div className="flex items-center gap-2">
<GitBranch className="w-3.5 h-3.5 opacity-70" />
<span>{branch.name}</span>
</div>
{selectedBranch === branch.name && <Check className="w-3.5 h-3.5" />}
</div>
))}
</div>
</>
)}
</div> </div>
</div> </div>

35
src/vite-env.d.ts vendored
View File

@ -18,6 +18,25 @@ interface GitOperationResult {
stdout?: string stdout?: string
} }
interface SettingsOperationResult {
success: boolean
message?: string
data?: unknown
}
interface CacheOperationResult {
success: boolean
message?: string
data?: any
exists?: boolean
}
interface WindowOperationResult {
success: boolean
message?: string
isMaximized?: boolean
}
interface ElectronAPI { interface ElectronAPI {
file: { file: {
save: (content: string, filename?: string) => Promise<FileOperationResult> save: (content: string, filename?: string) => Promise<FileOperationResult>
@ -33,6 +52,22 @@ interface ElectronAPI {
get: (key?: string) => Promise<SettingsOperationResult> get: (key?: string) => Promise<SettingsOperationResult>
save: (key: string, value: unknown) => Promise<SettingsOperationResult> save: (key: string, value: unknown) => Promise<SettingsOperationResult>
} }
cache: {
setJSON: (key: string, data: any, config?: { maxAge?: number; namespace?: string }) => Promise<CacheOperationResult>
getJSON: (key: string, config?: { maxAge?: number; namespace?: string }) => Promise<CacheOperationResult>
setBinary: (key: string, buffer: any, type?: 'binary' | 'image', config?: { maxAge?: number; namespace?: string }) => Promise<CacheOperationResult>
getBinary: (key: string, config?: { maxAge?: number; namespace?: string }) => Promise<CacheOperationResult>
has: (key: string, config?: { namespace?: string }) => Promise<CacheOperationResult>
delete: (key: string, namespace?: string) => Promise<CacheOperationResult>
clearNamespace: (namespace: string) => Promise<CacheOperationResult>
clearAll: () => Promise<CacheOperationResult>
getStats: () => Promise<CacheOperationResult>
}
window: {
minimize: () => Promise<WindowOperationResult>
maximize: () => Promise<WindowOperationResult>
close: () => Promise<WindowOperationResult>
}
} }
declare global { declare global {

View File

@ -59,6 +59,13 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
// 不重写路径,直接转发 // 不重写路径,直接转发
},
// 代理仿真资源 API
'/zichan-api': {
target: 'http://172.16.1.144:8081',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/zichan-api/, ''),
} }
} }
} }