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/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"antd": "^5.25.4",
|
||||||
|
"axios": "^1.9.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-scripts": "5.0.1",
|
"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;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
.stats {
|
||||||
height: 40vmin;
|
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;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
.image-wrapper:hover .image-hover-info {
|
||||||
.App-logo {
|
opacity: 1;
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-header {
|
.image-wrapper {
|
||||||
background-color: #282c34;
|
position: relative;
|
||||||
min-height: 100vh;
|
}
|
||||||
|
|
||||||
|
/* 放大图片样式 */
|
||||||
|
.enlarged-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: 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;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-link {
|
.link-btn:hover {
|
||||||
color: #61dafb;
|
background-color: #73d13d;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
.selected-count {
|
||||||
from {
|
margin-left: 15px;
|
||||||
transform: rotate(0deg);
|
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 './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() {
|
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 (
|
return (
|
||||||
<div className="App">
|
<div className="app">
|
||||||
<header className="App-header">
|
<header className="app-header">
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<h1>上传图片管理系统</h1>
|
||||||
<p>
|
<div className="stats">
|
||||||
Edit <code>src/App.js</code> and save to reload.
|
<span className="total-count">总图片数: {images.length}</span>
|
||||||
</p>
|
<span className="filtered-count">当前显示: {filteredImages.length}</span>
|
||||||
<a
|
</div>
|
||||||
className="App-link"
|
<div className="controls">
|
||||||
href="https://reactjs.org"
|
<div className="filter-controls">
|
||||||
target="_blank"
|
<div className="token-input">
|
||||||
rel="noopener noreferrer"
|
<label htmlFor="token">授权Token:</label>
|
||||||
>
|
<input
|
||||||
Learn React
|
id="token"
|
||||||
</a>
|
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>
|
</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>
|
</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