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