This commit is contained in:
Voge1imkafig 2025-07-04 15:00:43 +08:00
parent 076ad66136
commit d5f328a288
13 changed files with 2458 additions and 36 deletions

1053
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

BIN
public/pictures/R-C.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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;
}
}

View File

@ -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;

3
src/components/DEFINE.js Normal file
View File

@ -0,0 +1,3 @@
const DTAI_SITE = 'http://pms.dtyx.net:9158';
export default DTAI_SITE;

View File

@ -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;

View File

@ -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('图片关联失败');
};

View File

@ -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;

165
src/components/LinkForm.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

3
src/constants.js Normal file
View File

@ -0,0 +1,3 @@
export const SHOOTING_METHOD_OPTIONS = [
'MANUAL', 'AUTO', 'TRIPOD', 'DRONE', 'ROBOT'
];