no message
This commit is contained in:
parent
725409a24b
commit
271c11b453
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 |
|
|
@ -6,8 +6,8 @@
|
|||
<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" />
|
||||
<title>AssetPro</title>
|
||||
<script type="module" crossorigin src="./assets/index-C1_JoRdR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Cnn4LfhN.css">
|
||||
<script type="module" crossorigin src="./assets/index-Cst68Kk3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BAlDckV1.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -14,8 +14,8 @@ x64:
|
|||
nodeModuleFilePatterns: []
|
||||
nsis:
|
||||
script: |-
|
||||
!include "D:\Project\AssetPro\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
|
||||
!addincludedir "D:\Project\AssetPro\node_modules\app-builder-lib\templates\nsis\include"
|
||||
!include "D:\Project\AssetPro2\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
|
||||
!addincludedir "D:\Project\AssetPro2\node_modules\app-builder-lib\templates\nsis\include"
|
||||
!macro _isUpdated _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "updated"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
|
|
@ -87,7 +87,7 @@ nsis:
|
|||
!insertmacro MUI_LANGUAGE "Vietnamese"
|
||||
!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"
|
||||
|
||||
Var newStartMenuLink
|
||||
|
|
|
|||
Binary file not shown.
12
src/App.tsx
12
src/App.tsx
|
|
@ -5,6 +5,8 @@ import AssetLibrary from '@/pages/assets/AssetLibrary'
|
|||
import UploadResource from '@/pages/assets/UploadResource'
|
||||
import ProjectLibrary from '@/pages/projects/ProjectLibrary'
|
||||
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 { DownloadManagerProvider } from '@/components/DownloadManager'
|
||||
|
|
@ -26,8 +28,12 @@ function App() {
|
|||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/upload" element={<UploadResource />} />
|
||||
<Route path="/app/projects/new" element={<NewProject />} />
|
||||
{/* Full width pages with Header but no Sidebar */}
|
||||
<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 */}
|
||||
<Route path="/app" element={<MainLayout />}>
|
||||
|
|
@ -38,7 +44,7 @@ function App() {
|
|||
<Route path="prototypes" element={<PlaceholderPage title="原型库" />} />
|
||||
<Route path="design" element={<PlaceholderPage title="设计库" />} />
|
||||
<Route path="projects" element={<ProjectLibrary />} />
|
||||
<Route path="files" element={<PlaceholderPage title="文件库" />} />
|
||||
<Route path="files" element={<FileLibrary />} />
|
||||
<Route path="cases" element={<PlaceholderPage title="案例库" />} />
|
||||
<Route path="pending" element={<PlaceholderPage title="待审核" />} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -14,13 +14,34 @@ const apiClient = axios.create({
|
|||
timeout: 60000,
|
||||
headers: {
|
||||
'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(
|
||||
(response) => response,
|
||||
(response) => {
|
||||
console.log('<<< Gitea Response JSON:', response.data);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
console.error('API Error:', error);
|
||||
return Promise.reject(error);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ interface DownloadItem {
|
|||
}
|
||||
|
||||
interface DownloadContextType {
|
||||
startDownload: (repoUrl: string, targetDir: string) => Promise<void>
|
||||
startDownload: (repoUrl: string, targetDir: string, branch?: string) => Promise<void>
|
||||
activeDownloads: DownloadItem[]
|
||||
}
|
||||
|
||||
|
|
@ -40,6 +40,9 @@ export function DownloadManagerProvider({ children }: { children: React.ReactNod
|
|||
}, [activeDownloads])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
|
||||
// 监听 Git 进度
|
||||
window.electronAPI.git.onProgress((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 newItem: DownloadItem = {
|
||||
id,
|
||||
|
|
@ -101,7 +104,8 @@ export function DownloadManagerProvider({ children }: { children: React.ReactNod
|
|||
setActiveDownloads(prev => [...prev, newItem])
|
||||
|
||||
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) {
|
||||
setActiveDownloads(prev => prev.map(item =>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import { Search, Plus, User, Minus, Square, X } from 'lucide-react'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useToast } from '@/components/ui/toast-provider'
|
||||
|
||||
export function Header() {
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.electronAPI?.window.minimize()
|
||||
|
|
@ -23,10 +25,10 @@ export function Header() {
|
|||
{/* Left: Logo */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center">
|
||||
{/* Simple Logo Icon Placeholder */}
|
||||
<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>
|
||||
{/* Simple Logo Icon Placeholder */}
|
||||
<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>
|
||||
|
|
@ -54,11 +56,36 @@ export function Header() {
|
|||
|
||||
<div className="h-6 w-px bg-slate-200 mx-2"></div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<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 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">
|
||||
<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>
|
||||
<span className="text-sm font-medium text-slate-600">欢迎!XXX</span>
|
||||
</div>
|
||||
|
||||
{/* Window Controls */}
|
||||
|
|
|
|||
|
|
@ -41,4 +41,12 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|||
<ToastContainer toasts={toasts} onClose={removeToast} />
|
||||
</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
|
||||
}
|
||||
|
|
@ -173,7 +173,7 @@ export const cacheUtils = {
|
|||
*/
|
||||
async has(key: string, config?: { namespace?: string }): Promise<boolean> {
|
||||
const result = await window.electronAPI.cache.has(key, config)
|
||||
return result.success ? result.exists : false
|
||||
return result.success ? (result.exists ?? false) : false
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@ import { Outlet } from 'react-router-dom'
|
|||
import { Sidebar } from '@/components/layout/Sidebar'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
|
||||
export default function MainLayout() {
|
||||
interface MainLayoutProps {
|
||||
showSidebar?: boolean
|
||||
}
|
||||
|
||||
export default function MainLayout({ showSidebar = true }: MainLayoutProps) {
|
||||
return (
|
||||
<div className="h-screen w-full bg-slate-50 flex flex-col overflow-hidden">
|
||||
{/* Header at the top */}
|
||||
<Header />
|
||||
|
||||
|
||||
{/* Main content area below header */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Sidebar on the left */}
|
||||
<Sidebar />
|
||||
|
||||
{showSidebar && <Sidebar />}
|
||||
|
||||
{/* Page Content on the right */}
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<Outlet />
|
||||
|
|
|
|||
|
|
@ -4,16 +4,43 @@ import { User, Lock, Power } from 'lucide-react'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
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() {
|
||||
const navigate = useNavigate()
|
||||
const [username, setUsername] = useState('test1')
|
||||
const [password, setPassword] = useState('')
|
||||
const { showToast } = useToast()
|
||||
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 = () => {
|
||||
// 简单的登录模拟
|
||||
console.log('Login with:', username, password)
|
||||
navigate('/app')
|
||||
const handleLogin = async () => {
|
||||
if (!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')
|
||||
} 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 (
|
||||
|
|
@ -21,16 +48,16 @@ export default function Login() {
|
|||
{/* 顶部拖动区域 - 降低 z-index 以免覆盖交互元素 */}
|
||||
<div className="absolute top-0 left-0 right-0 h-12 drag-region z-40" />
|
||||
|
||||
<div
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
//background: 'linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%)', // 默认淡蓝色渐变
|
||||
backgroundImage: 'url("/src/assets/images/登录背景.png")', // 替换为实际背景图路径
|
||||
//background: 'linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%)', // 默认淡蓝色渐变
|
||||
backgroundImage: `url("${bgImage}")`, // 使用导入的图片以确保打包后路径正确
|
||||
backgroundSize: 'cover',
|
||||
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">
|
||||
<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="flex w-full max-w-6xl items-center justify-between gap-16">
|
||||
|
||||
|
||||
{/* 左侧装饰区域 (对应图中的文件夹 3D 图标) */}
|
||||
<div className="hidden lg:flex flex-1 items-center justify-center">
|
||||
{/*
|
||||
{/*
|
||||
TODO: 这里是放置左侧 3D 文件夹插图的位置
|
||||
目前使用一个透明占位符,实际背景图中如果包含了左侧插图,这里可以保持为空或者放置额外的动态元素
|
||||
*/}
|
||||
<div className="w-full h-full min-h-[400px]"></div>
|
||||
<div className="w-full h-full min-h-[400px]"></div>
|
||||
</div>
|
||||
|
||||
{/* 右侧登录卡片 */}
|
||||
|
|
@ -89,16 +116,24 @@ export default function Login() {
|
|||
</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"
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
登 录
|
||||
{loading ? '登 录 中...' : '登 录'}
|
||||
</Button>
|
||||
|
||||
{/* 底部链接 */}
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Filter, Download, Eye, Heart } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardFooter } from '@/components/ui/card'
|
||||
|
|
@ -9,29 +9,41 @@ const tabs = [
|
|||
"全部", "地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"
|
||||
]
|
||||
|
||||
interface Asset {
|
||||
id: string
|
||||
title: string
|
||||
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: ''
|
||||
}))
|
||||
import { getSimulationUploadList } from '@/api/simulation'
|
||||
import { SimulationResource } from '@/api/simulationTypes'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function AssetLibrary() {
|
||||
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 (
|
||||
<div className="bg-white rounded-xl shadow-sm min-h-full flex flex-col">
|
||||
|
|
@ -56,7 +68,7 @@ export default function AssetLibrary() {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<Button variant="ghost" size="sm" className="gap-2 text-slate-600 hover:text-[#0e7490]">
|
||||
<Filter className="w-4 h-4" />
|
||||
筛选
|
||||
|
|
@ -65,17 +77,25 @@ export default function AssetLibrary() {
|
|||
|
||||
{/* Asset Grid */}
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{mockAssets.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" />
|
||||
{/* You would put <img src={asset.imageUrl} ... /> here */}
|
||||
|
||||
{/* 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">
|
||||
{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">
|
||||
{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">
|
||||
<Eye className="w-4 h-4" />
|
||||
</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">
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<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.author}</span>
|
||||
<span className="text-slate-300">|</span>
|
||||
<span>{asset.date}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="w-3 h-3" />
|
||||
<span>{asset.downloads}</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</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.name}>
|
||||
{asset.name}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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">
|
||||
{tag.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,43 @@
|
|||
import { useState, type ChangeEvent } from 'react'
|
||||
import { useState, type ChangeEvent, useContext } from 'react'
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
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() {
|
||||
const navigate = useNavigate()
|
||||
const toast = useContext(ToastContext)
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
name: '12',
|
||||
type: 'Unity插件/场景',
|
||||
category: '天气',
|
||||
tags: ['Unity', 'Unreal', '3D模型'],
|
||||
description: '',
|
||||
detailDescription: '',
|
||||
name: '虚拟仿真实验平台',
|
||||
stores: '插件库',
|
||||
type: '地形',
|
||||
category: '教学平台',
|
||||
tags: ['仿真', '教学', '3D'],
|
||||
description: '用于物理化学实验的虚拟仿真平台',
|
||||
detailDescription: '该平台支持多种实验场景,包括光学、力学、化学等实验模块',
|
||||
urls: 'http://example.com',
|
||||
versions: 'v1.0.0',
|
||||
env: 'Windows 10/Linux',
|
||||
createdBy: 'Admin',
|
||||
files: [] as File[],
|
||||
})
|
||||
|
||||
|
|
@ -46,50 +66,73 @@ export default function UploadResource() {
|
|||
setIsUploading(true)
|
||||
let progress = 0
|
||||
const interval = setInterval(() => {
|
||||
progress += 10
|
||||
progress += 20
|
||||
setUploadProgress(progress)
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval)
|
||||
setIsUploading(false)
|
||||
}
|
||||
}, 200)
|
||||
|
||||
}, 100)
|
||||
|
||||
setFormData({ ...formData, files: Array.from(e.target.files) })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-slate-50 flex flex-col overflow-hidden">
|
||||
{/* Custom Header for Upload Page */}
|
||||
<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 handleSubmit = async () => {
|
||||
try {
|
||||
setIsUploading(true)
|
||||
|
||||
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 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(-1)}
|
||||
<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" />
|
||||
|
|
@ -109,7 +152,7 @@ export default function UploadResource() {
|
|||
<div className="absolute inset-0 bg-slate-200 border-t border-dashed border-slate-300" />
|
||||
{/* Active Connection Line - 已完成的连接线 */}
|
||||
{currentStep > 1 && (
|
||||
<div
|
||||
<div
|
||||
className="absolute left-0 top-0 h-[1px] bg-[#0e7490] transition-all duration-300"
|
||||
style={{
|
||||
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
|
||||
|
|
@ -117,15 +160,15 @@ export default function UploadResource() {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Step Circles and Labels - 使用 justify-between 让第一个和最后一个分别在两端 */}
|
||||
<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 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",
|
||||
currentStep >= step.number
|
||||
? "bg-[#0e7490] text-white"
|
||||
currentStep >= step.number
|
||||
? "bg-[#0e7490] text-white"
|
||||
: "bg-slate-100 text-slate-400"
|
||||
)}>
|
||||
{currentStep > step.number ? <CheckCircle2 className="w-5 h-5" /> : step.number}
|
||||
|
|
@ -145,29 +188,54 @@ export default function UploadResource() {
|
|||
{currentStep === 1 && (
|
||||
<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>
|
||||
|
||||
|
||||
<div className="space-y-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>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={e => setFormData({...formData, name: e.target.value})}
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
||||
/>
|
||||
</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">
|
||||
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span>资源类型</label>
|
||||
<Input
|
||||
value={formData.type}
|
||||
onChange={e => setFormData({...formData, type: e.target.value})}
|
||||
/>
|
||||
<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}
|
||||
onChange={e => setFormData({ ...formData, type: e.target.value })}
|
||||
>
|
||||
{RESOURCE_LIBRARIES[formData.stores]?.map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
<Input
|
||||
value={formData.category}
|
||||
onChange={e => setFormData({...formData, category: e.target.value})}
|
||||
<Input
|
||||
value={formData.category}
|
||||
onChange={e => setFormData({ ...formData, category: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -181,7 +249,7 @@ export default function UploadResource() {
|
|||
<button className="hover:text-cyan-200"><X className="w-3 h-3" /></button>
|
||||
</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">
|
||||
{tag}
|
||||
</button>
|
||||
|
|
@ -193,8 +261,8 @@ export default function UploadResource() {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-between pt-10">
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -207,9 +275,9 @@ export default function UploadResource() {
|
|||
|
||||
{/* 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">
|
||||
<input
|
||||
type="file"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
<input
|
||||
type="file"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<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 */}
|
||||
{(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="w-10 h-10 bg-red-100 rounded flex items-center justify-center text-red-500 font-bold text-xs">
|
||||
PDF
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span className="font-medium text-slate-700">这是一个文件的名字.pdf</span>
|
||||
<span className="text-slate-400">34.01KB</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#0e7490] transition-all duration-300"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-slate-400 hover:text-slate-600">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<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">
|
||||
PDF
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span className="font-medium text-slate-700">这是一个文件的名字.pdf</span>
|
||||
<span className="text-slate-400">34.01KB</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#0e7490] transition-all duration-300"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-slate-400 hover:text-slate-600">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<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="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">
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-xs font-medium text-slate-600 mb-1">点击上传缩略图</p>
|
||||
<p className="text-[10px] text-slate-400">支持格式:JPG/PNG | 建议尺寸:800x600px | 最大文件大小:5MB</p>
|
||||
</div>
|
||||
<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="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">
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-xs font-medium text-slate-600 mb-1">点击上传缩略图</p>
|
||||
<p className="text-[10px] text-slate-400">支持格式:JPG/PNG | 建议尺寸:800x600px | 最大文件大小:5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">保存草稿</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handlePrev}>上一步</Button>
|
||||
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]">下一步</Button>
|
||||
</div>
|
||||
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">保存草稿</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handlePrev}>上一步</Button>
|
||||
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]">下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -269,57 +337,69 @@ export default function UploadResource() {
|
|||
{currentStep === 3 && (
|
||||
<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>
|
||||
|
||||
|
||||
<div className="space-y-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>
|
||||
<Textarea
|
||||
placeholder="请简要描述资源的功能、特点和使用场景(200字以内)"
|
||||
<Textarea
|
||||
placeholder="请简要描述资源的功能、特点和使用场景(200字以内)"
|
||||
className="min-h-[100px]"
|
||||
value={formData.description}
|
||||
onChange={e => setFormData({...formData, description: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_1fr] gap-4">
|
||||
<label className="text-right text-sm text-slate-500 pt-2">详细说明</label>
|
||||
<Textarea
|
||||
placeholder="请详细描述资源的安装、使用、适配环境等信息(可选)"
|
||||
<Textarea
|
||||
placeholder="请详细描述资源的安装、使用、适配环境等信息(可选)"
|
||||
className="min-h-[150px]"
|
||||
value={formData.detailDescription}
|
||||
onChange={e => setFormData({...formData, detailDescription: e.target.value})}
|
||||
onChange={e => setFormData({ ...formData, detailDescription: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_1fr] items-center gap-4">
|
||||
<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 className="grid grid-cols-[100px_1fr] items-center gap-4">
|
||||
<label className="text-right text-sm text-slate-500">资源类型</label>
|
||||
<Input placeholder="如V1.0、2021版(可选)" defaultValue="如V1.0、2021版(可选)" />
|
||||
<label className="text-right text-sm text-slate-500">版本号</label>
|
||||
<Input
|
||||
placeholder="如V1.0.0"
|
||||
value={formData.versions}
|
||||
onChange={e => setFormData({ ...formData, versions: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_1fr] items-center gap-4">
|
||||
<label className="text-right text-sm text-slate-500">适配环境</label>
|
||||
<Input placeholder="如Unity2021.3、Windows 10(可选)" defaultValue="如Unity2021.3、Windows 10(可选)" />
|
||||
<label className="text-right text-sm text-slate-500">运行环境</label>
|
||||
<Input
|
||||
placeholder="如Windows 10/Linux"
|
||||
value={formData.env}
|
||||
onChange={e => setFormData({ ...formData, env: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_1fr] items-center gap-4">
|
||||
<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">
|
||||
这是一个文件的名字.pdf
|
||||
这是一个文件的名字.pdf
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-10">
|
||||
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">保存草稿</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handlePrev}>上一步</Button>
|
||||
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]">下一步</Button>
|
||||
</div>
|
||||
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">保存草稿</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handlePrev}>上一步</Button>
|
||||
<Button onClick={handleNext} className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]">下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -327,14 +407,18 @@ export default function UploadResource() {
|
|||
{currentStep === 4 && (
|
||||
<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>
|
||||
|
||||
|
||||
<div className="bg-slate-50 rounded-lg p-6 space-y-4 text-sm">
|
||||
<h4 className="font-bold text-slate-800 mb-4">确认并提交</h4>
|
||||
|
||||
|
||||
<div className="grid grid-cols-[80px_1fr] gap-4">
|
||||
<span className="text-slate-500">资源名称:</span>
|
||||
<span className="text-slate-900 font-medium">{formData.name}</span>
|
||||
</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">
|
||||
<span className="text-slate-500">资源类型:</span>
|
||||
<span className="text-slate-900">{formData.type}</span>
|
||||
|
|
@ -358,31 +442,38 @@ export default function UploadResource() {
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<ul className="list-disc list-inside space-y-1 text-orange-700/80 text-xs ml-1">
|
||||
<li>提交的资源将进入审核流程,审核通过后将在平台展示</li>
|
||||
<li>请确保上传的资源拥有合法权,禁止上传侵权内容</li>
|
||||
<li>审核周期一般为1-3个工作日,审核结果将通过短信/邮件通知</li>
|
||||
<li>如需修改已提交的资源,请在审核完成前撤回</li>
|
||||
</ul>
|
||||
<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>
|
||||
提交须知
|
||||
</div>
|
||||
<ul className="list-disc list-inside space-y-1 text-orange-700/80 text-xs ml-1">
|
||||
<li>提交的资源将进入审核流程,审核通过后将在平台展示</li>
|
||||
<li>请确保上传的资源拥有合法权,禁止上传侵权内容</li>
|
||||
<li>审核周期一般为1-3个工作日,审核结果将通过短信/邮件通知</li>
|
||||
<li>如需修改已提交的资源,请在审核完成前撤回</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="agreement" className="rounded border-slate-300 text-[#0e7490] focus:ring-[#0e7490]" />
|
||||
<label htmlFor="agreement" className="text-sm text-slate-600">
|
||||
我已阅读并同意 <a href="#" className="text-[#0e7490] underline">《虚拟仿真课程资源管理库用户协议》</a> 和 <a href="#" className="text-[#0e7490] underline">《资源上传规范》</a>
|
||||
</label>
|
||||
<input type="checkbox" id="agreement" className="rounded border-slate-300 text-[#0e7490] focus:ring-[#0e7490]" />
|
||||
<label htmlFor="agreement" className="text-sm text-slate-600">
|
||||
我已阅读并同意 <a href="#" className="text-[#0e7490] underline">《虚拟仿真课程资源管理库用户协议》</a> 和 <a href="#" className="text-[#0e7490] underline">《资源上传规范》</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">保存草稿</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handlePrev}>上一步</Button>
|
||||
<Button className="bg-[#0e7490] hover:bg-[#0891b2] min-w-[100px]">提交审核</Button>
|
||||
</div>
|
||||
<Button variant="secondary" className="bg-slate-100 text-slate-600 hover:bg-slate-200">保存草稿</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={handlePrev} disabled={isUploading}>上一步</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>
|
||||
)}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 64位操作系统,Unity 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ArrowLeft, Power } from 'lucide-react'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
|
@ -93,30 +93,7 @@ export default function NewProject() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-slate-50 flex flex-col overflow-hidden">
|
||||
{/* 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>
|
||||
<div className="flex flex-col h-full bg-slate-50">
|
||||
|
||||
{/* Main Content */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ import {
|
|||
ArrowDownToLine,
|
||||
GitCommitHorizontal,
|
||||
User,
|
||||
Calendar
|
||||
Calendar,
|
||||
GitBranch,
|
||||
ChevronsUpDown,
|
||||
Check
|
||||
} from 'lucide-react'
|
||||
|
||||
|
||||
|
|
@ -107,6 +110,11 @@ export default function ProjectLibrary() {
|
|||
const [commits, setCommits] = useState<GiteaCommit[]>([]) // Git Commits
|
||||
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数据变化时,构建树形结构
|
||||
useEffect(() => {
|
||||
if (!repos || repos.length === 0) return
|
||||
|
|
@ -209,7 +217,30 @@ export default function ProjectLibrary() {
|
|||
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)
|
||||
setCommits(data)
|
||||
|
||||
|
|
@ -229,6 +260,30 @@ export default function ProjectLibrary() {
|
|||
}
|
||||
|
||||
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])
|
||||
|
||||
/**
|
||||
|
|
@ -517,7 +572,7 @@ export default function ProjectLibrary() {
|
|||
const targetPath = result.path
|
||||
|
||||
// 2. 委托给 DownloadManager 开始下载(支持排队和进度显示)
|
||||
startDownload(selectedRepo.clone_url, targetPath)
|
||||
startDownload(selectedRepo.clone_url, targetPath, selectedBranch)
|
||||
|
||||
} catch (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="space-y-2">
|
||||
<div className="text-slate-400 text-xs">分支</div>
|
||||
<div className="text-slate-700 font-medium">
|
||||
{selectedRepo.default_branch}(最新提交: {new Date(selectedRepo.updated_at).toLocaleDateString()})
|
||||
<div className="text-slate-400 text-xs flex items-center gap-1">
|
||||
<GitBranch className="w-3 h-3" />
|
||||
分支
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,25 @@ interface GitOperationResult {
|
|||
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 {
|
||||
file: {
|
||||
save: (content: string, filename?: string) => Promise<FileOperationResult>
|
||||
|
|
@ -33,6 +52,22 @@ interface ElectronAPI {
|
|||
get: (key?: string) => 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 {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@ export default defineConfig({
|
|||
changeOrigin: true,
|
||||
secure: false,
|
||||
// 不重写路径,直接转发
|
||||
},
|
||||
// 代理仿真资源 API
|
||||
'/zichan-api': {
|
||||
target: 'http://172.16.1.144:8081',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/zichan-api/, ''),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue