This commit is contained in:
parent
076ad66136
commit
d5f328a288
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,8 @@
|
|||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"antd": "^5.25.4",
|
||||
"axios": "^1.9.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
704
src/App.css
704
src/App.css
|
@ -1,38 +1,702 @@
|
|||
.App {
|
||||
/* App.css */
|
||||
.app {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats span {
|
||||
padding: 5px 15px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.token-input, .user-filter {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.token-input label, .user-filter label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.token-input input, .user-filter select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
background-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f5222d;
|
||||
background-color: #fff1f0;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: #faad14;
|
||||
background-color: #fffbe6;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.image-card {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.image-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
height: 200px;
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
color: #bfbfbf;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.image-details {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.image-details h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.image-details p {
|
||||
margin: 6px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.image-details p strong {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 25px;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
font-size: 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.modal-body {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-image-preview {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modal-image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-metadata {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.image-metadata h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.metadata-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metadata-item label {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.metadata-item span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 图片悬停信息 */
|
||||
.image-hover-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
.image-wrapper:hover .image-hover-info {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 放大图片样式 */
|
||||
.enlarged-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.enlarged-content {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.enlarged-image {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.enlarged-content .close-btn {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 模态框中的图片预览 */
|
||||
.modal-preview .image-preview {
|
||||
cursor: zoom-in;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
/* 已选择图片样式 */
|
||||
.image-card.selected {
|
||||
border: 2px solid #1890ff;
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.image-checkbox {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 关联按钮样式 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
.link-btn:hover {
|
||||
background-color: #73d13d;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
.selected-count {
|
||||
margin-left: 15px;
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 关联模态框样式 */
|
||||
.link-modal {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.link-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.selected-images-preview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.small-preview .image-preview {
|
||||
max-height: 80px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.small-preview .image-wrapper {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.small-preview .image-hover-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 数字输入框样式 */
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* 关联模式下的图片卡片样式 */
|
||||
.image-card.linking-mode {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-card.linking-mode:hover {
|
||||
box-shadow: 0 0 0 2px #1890ff;
|
||||
}
|
||||
|
||||
/* 取消按钮样式 */
|
||||
.cancel-btn {
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: #ff7875;
|
||||
}
|
||||
|
||||
/* 详情模态框中的部件ID字段 */
|
||||
.image-metadata .metadata-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 关联模式下隐藏的悬停信息 */
|
||||
.linking-mode .image-hover-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 图片选择复选框样式 */
|
||||
.image-checkbox {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-checkbox input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.adaptive-link-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.adaptive-image {
|
||||
max-width: 100%;
|
||||
max-height: 40vh; /* 图片最大高度 */
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
overflow-y: auto; /* 仅表单区域可滚动 */
|
||||
flex-grow: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-group h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-item label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-item .value {
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.inline-fields {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
/* 主容器 (与modal-body配合) */
|
||||
.modal-body {
|
||||
display: flex;
|
||||
height: 80vh;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.modal-image-preview {
|
||||
flex: 0 0 45%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link-form-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* 表单标题区 */
|
||||
.form-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.image-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 表单网格布局 */
|
||||
.form-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-item label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-item select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.form-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
padding: 16px 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
padding: 10px 24px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
background: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 992px) {
|
||||
.modal-body {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.modal-image-preview {
|
||||
flex: auto;
|
||||
max-height: 40vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
268
src/App.js
268
src/App.js
|
@ -1,25 +1,261 @@
|
|||
import logo from './logo.svg';
|
||||
/**
|
||||
* 主应用组件
|
||||
* 图片管理系统入口,整合所有子组件
|
||||
* 主要功能:
|
||||
* - 显示图片列表
|
||||
* - 提供筛选功能
|
||||
* - 显示图片详情和关联表单
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './App.css';
|
||||
import { fetchImages, linkImagesToPart } from './components/ImageAPI.js';
|
||||
import ImagePreview from './components/ImagePreview';
|
||||
import LinkForm from './components/LinkForm';
|
||||
import UserFilter from './components/UserFilter';
|
||||
|
||||
function App() {
|
||||
const [images, setImages] = useState([]);
|
||||
const [filteredImages, setFilteredImages] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [token, setToken] = useState('');
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [uploadUsers, setUploadUsers] = useState(['all']);
|
||||
const [selectedUser, setSelectedUser] = useState('all');
|
||||
const [enlargedImage, setEnlargedImage] = useState(null);
|
||||
const [linkFormData, setLinkFormData] = useState({
|
||||
collectorId: '',
|
||||
collectorName: '',
|
||||
humidness: '',
|
||||
imagePaths: [],
|
||||
partId: '',
|
||||
shootingDistance: '',
|
||||
shootingMethod: '',
|
||||
shootingTimeBegin: '',
|
||||
shootingTimeEnd: '',
|
||||
temperatureMax: '',
|
||||
temperatureMin: '',
|
||||
weather: '',
|
||||
windLevel: ''
|
||||
});
|
||||
|
||||
// 获取图片数据
|
||||
const fetchImageData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const imagesData = await fetchImages(token);
|
||||
setImages(imagesData);
|
||||
|
||||
// 提取所有不重复的上传用户
|
||||
const users = [...new Set(imagesData.map(img => img.uploadUser || '未知'))];
|
||||
setUploadUsers(['all', ...users]);
|
||||
|
||||
// 设置默认用户筛选
|
||||
if (users.length > 0) {
|
||||
setSelectedUser(users[0]);
|
||||
setFilteredImages(imagesData.filter(img => img.uploadUser === users[0]));
|
||||
} else {
|
||||
setFilteredImages(imagesData);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
fetchImageData();
|
||||
}, []);
|
||||
|
||||
// 用户筛选处理
|
||||
useEffect(() => {
|
||||
if (selectedUser === 'all') {
|
||||
setFilteredImages(images);
|
||||
} else {
|
||||
setFilteredImages(images.filter(img => img.uploadUser === selectedUser));
|
||||
}
|
||||
}, [selectedUser, images]);
|
||||
|
||||
const handleTokenChange = (e) => {
|
||||
setToken(e.target.value);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchImageData();
|
||||
};
|
||||
|
||||
const handleImageClick = (image) => {
|
||||
setSelectedImage(image);
|
||||
setShowModal(true);
|
||||
|
||||
// 初始化关联表单数据
|
||||
setLinkFormData({
|
||||
collectorId: image.collectorId || '',
|
||||
collectorName: image.collectorName || '',
|
||||
humidness: image.humidness || '',
|
||||
imagePaths: [image.imagePath],
|
||||
partId: image.partId || '',
|
||||
shootingDistance: image.shootingDistance || '',
|
||||
shootingMethod: image.shootingMethod || '',
|
||||
shootingTimeBegin: image.shootingTimeBegin || '',
|
||||
shootingTimeEnd: image.shootingTimeEnd || '',
|
||||
temperatureMax: image.temperatureMax || '',
|
||||
temperatureMin: image.temperatureMin || '',
|
||||
weather: image.weather || '',
|
||||
windLevel: image.windLevel || ''
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
setSelectedImage(null);
|
||||
setEnlargedImage(null);
|
||||
};
|
||||
|
||||
const handleUserFilterChange = (e) => {
|
||||
setSelectedUser(e.target.value);
|
||||
};
|
||||
|
||||
// 处理关联表单字段变化
|
||||
const handleLinkFormChange = (fieldName, value) => {
|
||||
setLinkFormData(prev => ({
|
||||
...prev,
|
||||
[fieldName]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// 提交关联图片到机组
|
||||
const handleLinkImages = async () => {
|
||||
if (!linkFormData.partId) {
|
||||
alert('请填写部件ID');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await linkImagesToPart(token, linkFormData);
|
||||
alert('图片关联成功');
|
||||
closeModal();
|
||||
fetchImageData();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>上传图片管理系统</h1>
|
||||
<div className="stats">
|
||||
<span className="total-count">总图片数: {images.length}</span>
|
||||
<span className="filtered-count">当前显示: {filteredImages.length}</span>
|
||||
</div>
|
||||
<div className="controls">
|
||||
<div className="filter-controls">
|
||||
<div className="token-input">
|
||||
<label htmlFor="token">授权Token:</label>
|
||||
<input
|
||||
id="token"
|
||||
type="text"
|
||||
value={token}
|
||||
onChange={handleTokenChange}
|
||||
placeholder="输入授权token(可选)"
|
||||
/>
|
||||
</div>
|
||||
<UserFilter
|
||||
users={uploadUsers}
|
||||
selectedUser={selectedUser}
|
||||
onChange={handleUserFilterChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="action-buttons">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className={`refresh-btn ${loading ? 'loading' : ''}`}
|
||||
>
|
||||
{loading ? '加载中...' : '刷新数据'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="app-main">
|
||||
{loading && images.length === 0 ? (
|
||||
<div className="status-message loading">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="status-message error">{error}</div>
|
||||
) : filteredImages.length === 0 ? (
|
||||
<div className="status-message no-data">
|
||||
{selectedUser === 'all' ? '没有找到图片数据' : '该用户没有上传图片'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="image-grid">
|
||||
{filteredImages.map((image) => (
|
||||
<div
|
||||
key={image.imageId}
|
||||
className="image-card"
|
||||
onClick={() => handleImageClick(image)}
|
||||
>
|
||||
<ImagePreview image={image} />
|
||||
<div className="image-details">
|
||||
<h3>{image.imageName || '未命名图片'}</h3>
|
||||
<p><strong>上传用户:</strong> {image.uploadUser || '未知'}</p>
|
||||
<p><strong>拍摄时间:</strong> {image.shootingTime || '未知'}</p>
|
||||
{image.partId && <p><strong>关联部件:</strong> {image.partId}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* 图片详情模态框 */}
|
||||
{showModal && selectedImage && (
|
||||
<div className="modal-overlay" onClick={closeModal}>
|
||||
<div className="modal-content link-modal" onClick={e => e.stopPropagation()}>
|
||||
<button className="close-btn" onClick={closeModal}>×</button>
|
||||
<h2>图片关联信息</h2>
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="modal-image-preview">
|
||||
<ImagePreview
|
||||
image={selectedImage}
|
||||
isInModal
|
||||
onEnlarge={setEnlargedImage}
|
||||
/>
|
||||
</div>
|
||||
<LinkForm
|
||||
formData={linkFormData}
|
||||
image={selectedImage}
|
||||
onChange={handleLinkFormChange}
|
||||
onSubmit={handleLinkImages}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 放大图片的模态框 */}
|
||||
{enlargedImage && (
|
||||
<div className="enlarged-overlay" onClick={() => setEnlargedImage(null)}>
|
||||
<div className="enlarged-content" onClick={e => e.stopPropagation()}>
|
||||
<button className="close-btn" onClick={() => setEnlargedImage(null)}>×</button>
|
||||
<img src={enlargedImage} alt="放大图片" className="enlarged-image" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
|
@ -0,0 +1,3 @@
|
|||
const DTAI_SITE = 'http://pms.dtyx.net:9158';
|
||||
|
||||
export default DTAI_SITE;
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* 可编辑字段组件
|
||||
* 提供一个带有标签的可编辑输入字段,支持多种输入类型
|
||||
*
|
||||
* 用法:
|
||||
* <EditableField
|
||||
* label="字段标签"
|
||||
* value={fieldValue}
|
||||
* fieldName="fieldName"
|
||||
* onChange={handleChange}
|
||||
* type="text|number|datetime-local"
|
||||
* />
|
||||
*
|
||||
* @param {string} label - 字段标签文本
|
||||
* @param {string|number} value - 字段当前值
|
||||
* @param {string} fieldName - 字段名称,用于标识字段
|
||||
* @param {function} onChange - 字段值变化时的回调函数 (fieldName, newValue) => void
|
||||
* @param {string} [type="text"] - 输入类型,支持text/number/datetime-local等
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const EditableField = ({ label, value, fieldName, onChange, type = "text" }) => {
|
||||
const [fieldValue, setFieldValue] = useState(value || '');
|
||||
|
||||
const handleChange = (e) => {
|
||||
const newValue = e.target.value;
|
||||
setFieldValue(newValue);
|
||||
onChange(fieldName, newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="metadata-item">
|
||||
<label>{label}:</label>
|
||||
<input
|
||||
type={type}
|
||||
value={fieldValue}
|
||||
onChange={handleChange}
|
||||
className="editable-field"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableField;
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* 图片数据API模块
|
||||
* 封装所有与图片数据相关的API调用
|
||||
*
|
||||
* 主要函数:
|
||||
* - fetchImages(token): 获取图片列表
|
||||
* - linkImagesToPart(token, formData): 关联图片到部件
|
||||
*
|
||||
* 依赖:
|
||||
* - axios: HTTP请求库
|
||||
* - DTAI_SITE: API基础URL
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import DTAI_SITE from './DEFINE.js';
|
||||
|
||||
/**
|
||||
* 获取图片列表
|
||||
* @param {string} token - 授权token(可选)
|
||||
* @returns {Promise<Array>} 图片数据数组
|
||||
*/
|
||||
export const fetchImages = async (token) => {
|
||||
try {
|
||||
const headers = {};
|
||||
if (token) {
|
||||
headers.Authorization = token;
|
||||
}
|
||||
|
||||
const response = await axios.get(`${DTAI_SITE}/image/list/app-upload-images`, {
|
||||
headers
|
||||
});
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
return response.data.data || [];
|
||||
}
|
||||
throw new Error('获取图片失败');
|
||||
} catch (err) {
|
||||
throw new Error(err.message || '获取图片时发生错误');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关联图片到部件
|
||||
* @param {string} token - 授权token(可选)
|
||||
* @param {Object} formData - 关联表单数据
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
export const linkImagesToPart = async (token, formData) => {
|
||||
const headers = {};
|
||||
if (token) {
|
||||
headers.Authorization = token;
|
||||
}
|
||||
|
||||
// 转换数值类型字段
|
||||
const payload = {
|
||||
...formData,
|
||||
humidness: formData.humidness ? parseInt(formData.humidness) : undefined,
|
||||
shootingDistance: formData.shootingDistance ? parseInt(formData.shootingDistance) : undefined,
|
||||
temperatureMax: formData.temperatureMax ? parseFloat(formData.temperatureMax) : undefined,
|
||||
temperatureMin: formData.temperatureMin ? parseFloat(formData.temperatureMin) : undefined,
|
||||
windLevel: formData.windLevel ? parseInt(formData.windLevel) : undefined
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
DTAI_SITE + '/image/linkAppImagesToPart',
|
||||
payload,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error('图片关联失败');
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* 图片预览组件
|
||||
* 显示图片预览及基本信息,支持错误处理和点击放大
|
||||
*
|
||||
* 用法:
|
||||
* <ImagePreview
|
||||
* image={imageObject}
|
||||
* isInModal={false}
|
||||
* onEnlarge={handleEnlarge}
|
||||
* />
|
||||
*
|
||||
* @param {Object} image - 图片数据对象
|
||||
* @param {boolean} [isInModal=false] - 是否在模态框中显示
|
||||
* @param {function} [onEnlarge] - 点击放大回调函数
|
||||
*/
|
||||
import React from 'react';
|
||||
import DTAI_SITE from './DEFINE.js';
|
||||
|
||||
const ImagePreview = ({ image, isInModal = false, onEnlarge }) => {
|
||||
/**
|
||||
* 获取图片预览URL
|
||||
* @param {string} imagePath - 图片路径
|
||||
* @returns {string} 完整图片URL
|
||||
*/
|
||||
const getImagePreviewUrl = (imagePath) => {
|
||||
if (!imagePath) return '';
|
||||
return `${DTAI_SITE}${imagePath}`;
|
||||
};
|
||||
|
||||
const previewUrl = getImagePreviewUrl(image.imagePath);
|
||||
const imageSizeInfo = image.imageSize ? `大小: ${image.imageSize}` : '大小: 未知';
|
||||
const resolutionInfo = image.imageWidth && image.imageHeight
|
||||
? `分辨率: ${image.imageResolution}`
|
||||
: '分辨率: 未知';
|
||||
|
||||
return (
|
||||
<div className={`image-preview-container ${isInModal ? 'modal-preview' : ''}`}>
|
||||
{previewUrl ? (
|
||||
<div className="image-wrapper">
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={image.imageName || '预览图'}
|
||||
className="image-preview"
|
||||
onError={(e) => {
|
||||
e.target.onerror = null;
|
||||
e.target.src = './pictures/R-C.jpg';
|
||||
}}
|
||||
title={`${imageSizeInfo}\n${resolutionInfo}`}
|
||||
onClick={isInModal && onEnlarge ? () => onEnlarge(previewUrl) : undefined}
|
||||
/>
|
||||
<div className="image-hover-info">
|
||||
<div>{imageSizeInfo}</div>
|
||||
<div>{resolutionInfo}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="image-preview-placeholder">无预览图</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImagePreview;
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* 图片关联表单组件 - 与预览组件配合的布局
|
||||
* 自适应右侧表单区域,确保与左侧预览协调
|
||||
*/
|
||||
import React from 'react';
|
||||
import EditableField from './EditableField';
|
||||
import WeatherSelect from './weatherselect.js';
|
||||
|
||||
const SHOOTING_METHOD_OPTIONS = [
|
||||
'MANUAL', 'AUTO', 'TRIPOD', 'DRONE', 'ROBOT'
|
||||
];
|
||||
|
||||
const LinkForm = ({ formData, image, onChange, onSubmit, loading }) => {
|
||||
return (
|
||||
<div className="link-form-container">
|
||||
{/* 表单标题 */}
|
||||
<div className="form-header">
|
||||
<h3>图片关联信息</h3>
|
||||
<div className="image-meta">
|
||||
<span>{image.imageName}</span>
|
||||
<span>{image.imageWidth}×{image.imageHeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自适应表单网格 */}
|
||||
<div className="form-grid">
|
||||
{/* 第一组:核心信息 */}
|
||||
<div className="form-group">
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="部件ID"
|
||||
value={formData.partId}
|
||||
fieldName="partId"
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="采集员ID"
|
||||
value={formData.collectorId}
|
||||
fieldName="collectorId"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<EditableField
|
||||
label="姓名"
|
||||
value={formData.collectorName}
|
||||
fieldName="collectorName"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第二组:拍摄信息 */}
|
||||
<div className="form-group">
|
||||
<div className="form-row">
|
||||
<WeatherSelect
|
||||
value={formData.weather}
|
||||
onChange={value => onChange('weather', value)}
|
||||
/>
|
||||
<div className="form-item">
|
||||
<label>拍摄方式</label>
|
||||
<select
|
||||
value={formData.shootingMethod}
|
||||
onChange={(e) => onChange('shootingMethod', e.target.value)}
|
||||
>
|
||||
<option value="">选择方式</option>
|
||||
{SHOOTING_METHOD_OPTIONS.map(method => (
|
||||
<option key={method} value={method}>{method}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="拍摄距离"
|
||||
value={formData.shootingDistance}
|
||||
fieldName="shootingDistance"
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
suffix="米"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三组:环境信息 */}
|
||||
<div className="form-group">
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="最低温"
|
||||
value={formData.temperatureMin}
|
||||
fieldName="temperatureMin"
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
suffix="°C"
|
||||
/>
|
||||
<EditableField
|
||||
label="最高温"
|
||||
value={formData.temperatureMax}
|
||||
fieldName="temperatureMax"
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
suffix="°C"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="湿度"
|
||||
value={formData.humidness}
|
||||
fieldName="humidness"
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
suffix="%"
|
||||
/>
|
||||
<EditableField
|
||||
label="风力"
|
||||
value={formData.windLevel}
|
||||
fieldName="windLevel"
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
suffix="级"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第四组:时间信息 */}
|
||||
<div className="form-group">
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="开始时间"
|
||||
value={formData.shootingTimeBegin}
|
||||
fieldName="shootingTimeBegin"
|
||||
onChange={onChange}
|
||||
type="datetime-local"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<EditableField
|
||||
label="结束时间"
|
||||
value={formData.shootingTimeEnd}
|
||||
fieldName="shootingTimeEnd"
|
||||
onChange={onChange}
|
||||
type="datetime-local"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="form-actions">
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={loading}
|
||||
className="submit-button"
|
||||
>
|
||||
{loading ? '提交中...' : '保存关联信息'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkForm;
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 用户筛选组件
|
||||
* 提供按用户筛选图片的功能
|
||||
*
|
||||
* 用法:
|
||||
* <UserFilter
|
||||
* users={usersArray}
|
||||
* selectedUser={selectedUser}
|
||||
* onChange={handleUserChange}
|
||||
* disabled={false}
|
||||
* />
|
||||
*
|
||||
* @param {Array} users - 用户列表,包含'all'选项
|
||||
* @param {string} selectedUser - 当前选中的用户
|
||||
* @param {function} onChange - 用户选择变化回调
|
||||
* @param {boolean} disabled - 是否禁用选择框
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
const UserFilter = ({ users, selectedUser, onChange, disabled }) => {
|
||||
return (
|
||||
<div className="user-filter">
|
||||
<label htmlFor="user-filter">按用户筛选:</label>
|
||||
<select
|
||||
id="user-filter"
|
||||
value={selectedUser}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
{users.map(user => (
|
||||
<option key={user} value={user}>
|
||||
{user === 'all' ? '全部用户' : user}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserFilter;
|
|
@ -0,0 +1,76 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import DTAI_SITE from './DEFINE';
|
||||
|
||||
const WeatherSelect = ({ value, onChange }) => {
|
||||
const [weatherOptions, setWeatherOptions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 默认天气选项
|
||||
const defaultWeatherOptions = [
|
||||
{ value: 'sunny', label: '晴天' },
|
||||
{ value: 'cloudy', label: '多云' },
|
||||
{ value: 'light-rain', label: '小雨' },
|
||||
{ value: 'moderate-rain', label: '中雨' },
|
||||
{ value: 'heavy-rain', label: '大雨' },
|
||||
{ value: 'storm', label: '暴雨' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWeatherOptions = async () => {
|
||||
try {
|
||||
const response = await fetch(DTAI_SITE + '/weather-type/list');
|
||||
const data = await response.json();
|
||||
if (data.code === 0) {
|
||||
// 转换API数据格式
|
||||
const options = data.rows.map(item => ({
|
||||
value: item.weatherCode,
|
||||
label: item.chineseName
|
||||
}));
|
||||
// 如果API返回的列表不为空则使用API数据,否则使用默认选项
|
||||
setWeatherOptions(options.length > 0 ? options : defaultWeatherOptions);
|
||||
} else {
|
||||
// API返回错误时使用默认选项
|
||||
setWeatherOptions(defaultWeatherOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取天气选项失败:', error);
|
||||
// 请求失败时使用默认选项
|
||||
setWeatherOptions(defaultWeatherOptions);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWeatherOptions();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="metadata-item">
|
||||
<label>天气:</label>
|
||||
<select disabled>
|
||||
<option>加载天气数据中...</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="metadata-item">
|
||||
<label>天气:</label>
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">请选择天气</option>
|
||||
{weatherOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeatherSelect;
|
|
@ -0,0 +1,3 @@
|
|||
export const SHOOTING_METHOD_OPTIONS = [
|
||||
'MANUAL', 'AUTO', 'TRIPOD', 'DRONE', 'ROBOT'
|
||||
];
|
Loading…
Reference in New Issue