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 />} />
{/* Full width pages with Header but no Sidebar */}
<Route element={<MainLayout showSidebar={false} />}>
<Route path="/upload" element={<UploadResource />} /> <Route path="/upload" element={<UploadResource />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/app/projects/new" element={<NewProject />} /> <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()
@ -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="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"> <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" /> <User className="w-4 h-4 text-slate-400" />
</div> </div>
<span className="text-sm font-medium text-slate-600">XXX</span> {/* 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> </div>
{/* Window Controls */} {/* Window Controls */}

View File

@ -42,3 +42,11 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
</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,7 +2,11 @@ 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 */}
@ -11,7 +15,7 @@ export default function MainLayout() {
{/* 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">

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')
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') 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 (
@ -25,7 +52,7 @@ export default function Login() {
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',
}} }}
@ -92,13 +119,21 @@ export default function Login() {
<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">
@ -65,14 +77,22 @@ export default function AssetLibrary() {
{/* Asset Grid */} {/* Asset Grid */}
<div className="p-6"> <div className="p-6">
{loading ? (
<div className="flex h-64 items-center justify-center text-slate-400">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : assets.length === 0 ? (
<div className="flex h-64 items-center justify-center text-slate-400 text-sm">
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{mockAssets.map((asset) => ( {assets.map((asset) => (
<Card key={asset.id} className="overflow-hidden hover:shadow-xl transition-all duration-300 border-slate-100 group bg-white"> <Card key={asset.id} className="overflow-hidden hover:shadow-xl transition-all duration-300 border-slate-100 group bg-white">
{/* Image Area */} {/* Image Area */}
<div className="aspect-video bg-slate-100 relative overflow-hidden"> <div className="aspect-video bg-slate-100 relative overflow-hidden">
{/* Placeholder Gradient */} {/* 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" /> <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 */}
{/* Hover Actions Overlay */} {/* 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"> <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">
@ -89,14 +109,14 @@ export default function AssetLibrary() {
</div> </div>
<CardContent className="p-4 space-y-3"> <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}> <h3 className="font-bold text-slate-800 line-clamp-2 text-sm leading-relaxed min-h-[40px]" title={asset.name}>
{asset.title} {asset.name}
</h3> </h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{asset.tags.map(tag => ( {asset.tag && asset.tag.split(/[,]/).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"> <span key={tag} className="px-2 py-0.5 bg-slate-50 text-slate-400 text-xs rounded-sm border border-slate-100">
{tag} {tag.trim()}
</span> </span>
))} ))}
</div> </div>
@ -104,18 +124,19 @@ export default function AssetLibrary() {
<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"> <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"> <div className="flex items-center gap-2">
<span>{asset.author}</span> <span>{asset.createdBy || 'Unknown'}</span>
<span className="text-slate-300">|</span> <span className="text-slate-300">|</span>
<span>{asset.date}</span> <span>{asset.createdTime ? asset.createdTime.split(' ')[0] : '-'}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Download className="w-3 h-3" /> <Download className="w-3 h-3" />
<span>{asset.downloads}</span> <span>0</span>
</div> </div>
</CardFooter> </CardFooter>
</Card> </Card>
))} ))}
</div> </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" />
@ -120,7 +163,7 @@ export default function UploadResource() {
{/* 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",
@ -151,23 +194,48 @@ export default function UploadResource() {
<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
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.type} value={formData.type}
onChange={e => setFormData({...formData, type: e.target.value})} 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>
@ -277,7 +345,7 @@ export default function UploadResource() {
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>
@ -287,23 +355,35 @@ export default function UploadResource() {
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">
@ -335,6 +415,10 @@ export default function UploadResource() {
<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>
@ -380,8 +464,15 @@ export default function UploadResource() {
<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
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> </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/, ''),
} }
} }
} }