From 72bce2dbc8646f9273f53c1d2fa3f6fc1ebb2fe9 Mon Sep 17 00:00:00 2001 From: huanglinhuan Date: Wed, 3 Dec 2025 22:21:49 +0800 Subject: [PATCH] init commit --- .gitignore | 24 ++++ README.md | 34 +++++ package.json | 38 ++++++ public/index.html | 15 +++ src/App.css | 71 +++++++++++ src/App.js | 42 +++++++ src/components/PrivateRoute.js | 16 +++ src/context/AuthContext.js | 70 +++++++++++ src/index.css | 21 ++++ src/index.js | 12 ++ src/pages/AircraftDetail.css | 221 +++++++++++++++++++++++++++++++++ src/pages/AircraftDetail.js | 189 ++++++++++++++++++++++++++++ src/pages/AircraftList.css | 142 +++++++++++++++++++++ src/pages/AircraftList.js | 98 +++++++++++++++ src/pages/Login.css | 80 ++++++++++++ src/pages/Login.js | 77 ++++++++++++ 16 files changed, 1150 insertions(+) create mode 100755 .gitignore create mode 100755 README.md create mode 100755 package.json create mode 100755 public/index.html create mode 100755 src/App.css create mode 100755 src/App.js create mode 100755 src/components/PrivateRoute.js create mode 100755 src/context/AuthContext.js create mode 100755 src/index.css create mode 100755 src/index.js create mode 100755 src/pages/AircraftDetail.css create mode 100755 src/pages/AircraftDetail.js create mode 100755 src/pages/AircraftList.css create mode 100755 src/pages/AircraftList.js create mode 100755 src/pages/Login.css create mode 100755 src/pages/Login.js diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..8e58403 --- /dev/null +++ b/.gitignore @@ -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* + diff --git a/README.md b/README.md new file mode 100755 index 0000000..8d3f565 --- /dev/null +++ b/README.md @@ -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请求会自动转发到后端 + diff --git a/package.json b/package.json new file mode 100755 index 0000000..4b9f452 --- /dev/null +++ b/package.json @@ -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" +} + diff --git a/public/index.html b/public/index.html new file mode 100755 index 0000000..5735961 --- /dev/null +++ b/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + 机型信息管理系统 + + + +
+ + + diff --git a/src/App.css b/src/App.css new file mode 100755 index 0000000..01b577b --- /dev/null +++ b/src/App.css @@ -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; +} + diff --git a/src/App.js b/src/App.js new file mode 100755 index 0000000..d232743 --- /dev/null +++ b/src/App.js @@ -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 ( + + +
+ + } /> + + + + } + /> + + + + } + /> + } /> + +
+
+
+ ); +} + +export default App; + diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js new file mode 100755 index 0000000..b9a7dc4 --- /dev/null +++ b/src/components/PrivateRoute.js @@ -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
加载中...
; + } + + return user ? children : ; +}; + +export default PrivateRoute; + diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js new file mode 100755 index 0000000..46f8bff --- /dev/null +++ b/src/context/AuthContext.js @@ -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 {children}; +}; + diff --git a/src/index.css b/src/index.css new file mode 100755 index 0000000..1159aab --- /dev/null +++ b/src/index.css @@ -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; +} + diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..535f85e --- /dev/null +++ b/src/index.js @@ -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( + + + +); + diff --git a/src/pages/AircraftDetail.css b/src/pages/AircraftDetail.css new file mode 100755 index 0000000..8fed1d8 --- /dev/null +++ b/src/pages/AircraftDetail.css @@ -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; +} + diff --git a/src/pages/AircraftDetail.js b/src/pages/AircraftDetail.js new file mode 100755 index 0000000..3d83928 --- /dev/null +++ b/src/pages/AircraftDetail.js @@ -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 ( +
+
+
加载中...
+
+
+ ); + } + + if (!aircraft) { + return ( +
+
+
机型信息不存在
+
+
+ ); + } + + return ( +
+
+
+ +

机型详情

+
+
+
+ +
+
+
+
{aircraft.code}
+

{aircraft.name}

+
+ {aircraft.manufacturer} + {aircraft.type} +
+
+ +
+

机型简介

+

{aircraft.description}

+
+ +
+

技术规格

+
+
+ 最大载客量 + {aircraft.specifications.maxPassengers} 人 +
+
+ 最大航程 + {aircraft.specifications.maxRange} +
+
+ 巡航速度 + {aircraft.specifications.cruiseSpeed} +
+
+ 机长 + {aircraft.specifications.length} +
+
+ 翼展 + {aircraft.specifications.wingspan} +
+
+
+
+ +
+
+

相关资料

+ setSearchTerm(e.target.value)} + className="search-input-small" + /> +
+ + {pdfMaterials.length === 0 ? ( +
暂无相关资料
+ ) : ( +
+ {pdfMaterials.map((material) => ( +
+
+

{material.title}

+

{material.description}

+
+ 上传日期: {material.uploadDate} + 文件大小: {material.fileSize} +
+
+ +
+ ))} +
+ )} +
+
+
+ ); +}; + +export default AircraftDetail; + diff --git a/src/pages/AircraftList.css b/src/pages/AircraftList.css new file mode 100755 index 0000000..ccac66c --- /dev/null +++ b/src/pages/AircraftList.css @@ -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; +} + diff --git a/src/pages/AircraftList.js b/src/pages/AircraftList.js new file mode 100755 index 0000000..b64254a --- /dev/null +++ b/src/pages/AircraftList.js @@ -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 ( +
+
+
+

机型信息管理系统

+
+ 欢迎,{user?.name || user?.username} + +
+
+
+ +
+
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+ + {loading ? ( +
加载中...
+ ) : ( +
+ {aircrafts.length === 0 ? ( +
未找到机型信息
+ ) : ( + aircrafts.map((aircraft) => ( +
handleAircraftClick(aircraft.id)} + > +
{aircraft.code}
+

{aircraft.name}

+
+ {aircraft.manufacturer} + {aircraft.type} +
+
+
+ )) + )} +
+ )} +
+
+ ); +}; + +export default AircraftList; + diff --git a/src/pages/Login.css b/src/pages/Login.css new file mode 100755 index 0000000..6270f4b --- /dev/null +++ b/src/pages/Login.css @@ -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; +} + diff --git a/src/pages/Login.js b/src/pages/Login.js new file mode 100755 index 0000000..b1ce22c --- /dev/null +++ b/src/pages/Login.js @@ -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 ( +
+
+

机型信息管理系统

+
+
+ + setUsername(e.target.value)} + className="form-input" + placeholder="请输入用户名" + required + /> +
+
+ + setPassword(e.target.value)} + className="form-input" + placeholder="请输入密码" + required + /> +
+ {error &&
{error}
} + +
+
+

默认账号:admin / admin123

+
+
+
+ ); +}; + +export default Login; +