init commit
This commit is contained in:
24
.gitignore
vendored
Executable file
24
.gitignore
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
34
README.md
Executable file
34
README.md
Executable file
@@ -0,0 +1,34 @@
|
||||
# 机型信息管理系统 - 前端
|
||||
|
||||
## 技术栈
|
||||
- React 18
|
||||
- React Router
|
||||
- Axios
|
||||
- CSS3
|
||||
|
||||
## 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## 运行项目
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
项目将在 http://localhost:3000 启动
|
||||
|
||||
## 功能特性
|
||||
- 用户登录认证
|
||||
- 机型信息展示
|
||||
- 机型切换浏览
|
||||
- PDF资料查询和查看
|
||||
|
||||
## 默认登录信息
|
||||
- 用户名: `admin`
|
||||
- 密码: `admin123`
|
||||
|
||||
## 注意事项
|
||||
- 确保后端API服务已启动(默认端口3001)
|
||||
- 前端已配置代理,API请求会自动转发到后端
|
||||
|
||||
38
package.json
Executable file
38
package.json
Executable file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "department-web",
|
||||
"version": "1.0.0",
|
||||
"description": "机型信息管理系统前端",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"axios": "^1.6.2",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:3001"
|
||||
}
|
||||
|
||||
15
public/index.html
Executable file
15
public/index.html
Executable file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="机型信息管理系统" />
|
||||
<title>机型信息管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
71
src/App.css
Executable file
71
src/App.css
Executable file
@@ -0,0 +1,71 @@
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #52c41a;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
42
src/App.js
Executable file
42
src/App.js
Executable file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import Login from './pages/Login';
|
||||
import AircraftList from './pages/AircraftList';
|
||||
import AircraftDetail from './pages/AircraftDetail';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/aircraft"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<AircraftList />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/aircraft/:id"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<AircraftDetail />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to="/aircraft" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
16
src/components/PrivateRoute.js
Executable file
16
src/components/PrivateRoute.js
Executable file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const PrivateRoute = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: '20px', textAlign: 'center' }}>加载中...</div>;
|
||||
}
|
||||
|
||||
return user ? children : <Navigate to="/login" replace />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
|
||||
70
src/context/AuthContext.js
Executable file
70
src/context/AuthContext.js
Executable file
@@ -0,0 +1,70 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth必须在AuthProvider内使用');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 从localStorage恢复用户信息
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
const userInfo = localStorage.getItem('user');
|
||||
|
||||
if (token && userInfo) {
|
||||
setUser(JSON.parse(userInfo));
|
||||
// 设置axios默认header
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = async (username, password) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const { token, user } = response.data;
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
setUser(user);
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
return { success: true };
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || '登录失败,请检查用户名和密码'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setUser(null);
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
loading
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
21
src/index.css
Executable file
21
src/index.css
Executable file
@@ -0,0 +1,21 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
12
src/index.js
Executable file
12
src/index.js
Executable file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
221
src/pages/AircraftDetail.css
Executable file
221
src/pages/AircraftDetail.css
Executable file
@@ -0,0 +1,221 @@
|
||||
.aircraft-detail-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.aircraft-detail-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.aircraft-header {
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.aircraft-code-large {
|
||||
display: inline-block;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aircraft-name-large {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aircraft-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.manufacturer-badge,
|
||||
.type-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.manufacturer-badge {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.aircraft-description {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.aircraft-description h3,
|
||||
.aircraft-specifications h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aircraft-description p {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.aircraft-specifications {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.specs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pdf-materials-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-input-small {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.search-input-small:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.pdf-materials-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pdf-material-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e8e8e8;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pdf-material-card:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.pdf-material-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pdf-material-title {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pdf-material-description {
|
||||
margin: 0 0 12px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pdf-material-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
189
src/pages/AircraftDetail.js
Executable file
189
src/pages/AircraftDetail.js
Executable file
@@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import axios from 'axios';
|
||||
import './AircraftDetail.css';
|
||||
|
||||
const AircraftDetail = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuth();
|
||||
const [aircraft, setAircraft] = useState(null);
|
||||
const [pdfMaterials, setPdfMaterials] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchAircraftDetail();
|
||||
fetchPdfMaterials();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchPdfMaterials();
|
||||
}
|
||||
}, [searchTerm, id]);
|
||||
|
||||
const fetchAircraftDetail = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get(`/api/aircraft/${id}`);
|
||||
if (response.data.success) {
|
||||
setAircraft(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取机型详情失败:', error);
|
||||
if (error.response?.status === 401) {
|
||||
logout();
|
||||
navigate('/login');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPdfMaterials = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/pdf/aircraft/${id}`, {
|
||||
params: { search: searchTerm }
|
||||
});
|
||||
if (response.data.success) {
|
||||
setPdfMaterials(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取PDF资料失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewPdf = async (materialId) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/pdf/file/${materialId}`);
|
||||
if (response.data.success) {
|
||||
const fileUrl = `http://localhost:3001${response.data.data.fileUrl}`;
|
||||
window.open(fileUrl, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('打开PDF失败:', error);
|
||||
alert('无法打开PDF文件,请确保文件存在');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="aircraft-detail-page">
|
||||
<div className="container">
|
||||
<div className="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!aircraft) {
|
||||
return (
|
||||
<div className="aircraft-detail-page">
|
||||
<div className="container">
|
||||
<div className="error-state">机型信息不存在</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aircraft-detail-page">
|
||||
<header className="page-header">
|
||||
<div className="header-content">
|
||||
<button onClick={() => navigate('/aircraft')} className="btn btn-secondary">
|
||||
← 返回列表
|
||||
</button>
|
||||
<h1>机型详情</h1>
|
||||
<div></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container">
|
||||
<div className="aircraft-detail-card">
|
||||
<div className="aircraft-header">
|
||||
<div className="aircraft-code-large">{aircraft.code}</div>
|
||||
<h2 className="aircraft-name-large">{aircraft.name}</h2>
|
||||
<div className="aircraft-meta">
|
||||
<span className="manufacturer-badge">{aircraft.manufacturer}</span>
|
||||
<span className="type-badge">{aircraft.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aircraft-description">
|
||||
<h3>机型简介</h3>
|
||||
<p>{aircraft.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="aircraft-specifications">
|
||||
<h3>技术规格</h3>
|
||||
<div className="specs-grid">
|
||||
<div className="spec-item">
|
||||
<span className="spec-label">最大载客量</span>
|
||||
<span className="spec-value">{aircraft.specifications.maxPassengers} 人</span>
|
||||
</div>
|
||||
<div className="spec-item">
|
||||
<span className="spec-label">最大航程</span>
|
||||
<span className="spec-value">{aircraft.specifications.maxRange}</span>
|
||||
</div>
|
||||
<div className="spec-item">
|
||||
<span className="spec-label">巡航速度</span>
|
||||
<span className="spec-value">{aircraft.specifications.cruiseSpeed}</span>
|
||||
</div>
|
||||
<div className="spec-item">
|
||||
<span className="spec-label">机长</span>
|
||||
<span className="spec-value">{aircraft.specifications.length}</span>
|
||||
</div>
|
||||
<div className="spec-item">
|
||||
<span className="spec-label">翼展</span>
|
||||
<span className="spec-value">{aircraft.specifications.wingspan}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pdf-materials-section">
|
||||
<div className="section-header">
|
||||
<h3>相关资料</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索资料..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input-small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{pdfMaterials.length === 0 ? (
|
||||
<div className="empty-state">暂无相关资料</div>
|
||||
) : (
|
||||
<div className="pdf-materials-list">
|
||||
{pdfMaterials.map((material) => (
|
||||
<div key={material.id} className="pdf-material-card">
|
||||
<div className="pdf-material-info">
|
||||
<h4 className="pdf-material-title">{material.title}</h4>
|
||||
<p className="pdf-material-description">{material.description}</p>
|
||||
<div className="pdf-material-meta">
|
||||
<span>上传日期: {material.uploadDate}</span>
|
||||
<span>文件大小: {material.fileSize}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleViewPdf(material.id)}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
查看PDF
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AircraftDetail;
|
||||
|
||||
142
src/pages/AircraftList.css
Executable file
142
src/pages/AircraftList.css
Executable file
@@ -0,0 +1,142 @@
|
||||
.aircraft-list-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.aircraft-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.aircraft-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.aircraft-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.aircraft-code {
|
||||
display: inline-block;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aircraft-name {
|
||||
margin: 0 0 12px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aircraft-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.manufacturer {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 24px;
|
||||
color: #d9d9d9;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.aircraft-card:hover .card-arrow {
|
||||
color: #1890ff;
|
||||
transform: translateY(-50%) translateX(4px);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
98
src/pages/AircraftList.js
Executable file
98
src/pages/AircraftList.js
Executable file
@@ -0,0 +1,98 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import axios from 'axios';
|
||||
import './AircraftList.css';
|
||||
|
||||
const AircraftList = () => {
|
||||
const [aircrafts, setAircrafts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchAircrafts();
|
||||
}, [searchTerm]);
|
||||
|
||||
const fetchAircrafts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get('/api/aircraft/list', {
|
||||
params: { search: searchTerm }
|
||||
});
|
||||
if (response.data.success) {
|
||||
setAircrafts(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取机型列表失败:', error);
|
||||
if (error.response?.status === 401) {
|
||||
logout();
|
||||
navigate('/login');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAircraftClick = (id) => {
|
||||
navigate(`/aircraft/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aircraft-list-page">
|
||||
<header className="page-header">
|
||||
<div className="header-content">
|
||||
<h1>机型信息管理系统</h1>
|
||||
<div className="header-actions">
|
||||
<span className="user-info">欢迎,{user?.name || user?.username}</span>
|
||||
<button onClick={logout} className="btn btn-secondary">
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container">
|
||||
<div className="search-section">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索机型(名称、代码、制造商)..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">加载中...</div>
|
||||
) : (
|
||||
<div className="aircraft-grid">
|
||||
{aircrafts.length === 0 ? (
|
||||
<div className="empty-state">未找到机型信息</div>
|
||||
) : (
|
||||
aircrafts.map((aircraft) => (
|
||||
<div
|
||||
key={aircraft.id}
|
||||
className="aircraft-card"
|
||||
onClick={() => handleAircraftClick(aircraft.id)}
|
||||
>
|
||||
<div className="aircraft-code">{aircraft.code}</div>
|
||||
<h3 className="aircraft-name">{aircraft.name}</h3>
|
||||
<div className="aircraft-info">
|
||||
<span className="manufacturer">{aircraft.manufacturer}</span>
|
||||
<span className="type">{aircraft.type}</span>
|
||||
</div>
|
||||
<div className="card-arrow">→</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AircraftList;
|
||||
|
||||
80
src/pages/Login.css
Executable file
80
src/pages/Login.css
Executable file
@@ -0,0 +1,80 @@
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.login-hint p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
77
src/pages/Login.js
Executable file
77
src/pages/Login.js
Executable file
@@ -0,0 +1,77 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import './Login.css';
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
const result = await login(username, password);
|
||||
|
||||
if (result.success) {
|
||||
navigate('/aircraft');
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">机型信息管理系统</h1>
|
||||
<form onSubmit={handleSubmit} className="login-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary login-button"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="login-hint">
|
||||
<p>默认账号:admin / admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
||||
Reference in New Issue
Block a user