1526 lines
56 KiB
HTML
1526 lines
56 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>添加商品 - 大妈集市商户端</title>
|
|||
|
|
<style>
|
|||
|
|
* {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|||
|
|
background-color: #f5f5f5;
|
|||
|
|
color: #333;
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.container {
|
|||
|
|
max-width: 800px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.modal-header {
|
|||
|
|
padding: 20px 24px;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
background: #fafafa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.modal-title {
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.close-btn {
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
font-size: 20px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
color: #999;
|
|||
|
|
padding: 4px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
transition: background-color 0.3s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.close-btn:hover {
|
|||
|
|
background-color: #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.modal-body {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tabs-container {
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tabs {
|
|||
|
|
display: flex;
|
|||
|
|
padding: 0 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab {
|
|||
|
|
padding: 16px 20px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
border-bottom: 2px solid transparent;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #666;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab.active {
|
|||
|
|
color: #1890ff;
|
|||
|
|
border-bottom-color: #1890ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab:hover:not(.active) {
|
|||
|
|
color: #1890ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab-content {
|
|||
|
|
padding: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-group {
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-label {
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.required {
|
|||
|
|
color: #ff4d4f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-input, .form-select, .form-textarea {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
border: 1px solid #d9d9d9;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
transition: border-color 0.3s, box-shadow 0.3s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
|||
|
|
outline: none;
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-textarea {
|
|||
|
|
resize: vertical;
|
|||
|
|
min-height: 80px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-area {
|
|||
|
|
border: 2px dashed #d9d9d9;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
padding: 32px;
|
|||
|
|
text-align: center;
|
|||
|
|
background: #fafafa;
|
|||
|
|
transition: border-color 0.3s;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-area:hover {
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-area.dragover {
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
background: #f0f8ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-icon {
|
|||
|
|
font-size: 32px;
|
|||
|
|
color: #d9d9d9;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-text {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-highlight {
|
|||
|
|
color: #1890ff;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-hint {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 12px;
|
|||
|
|
margin-top: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-message {
|
|||
|
|
color: #ff4d4f;
|
|||
|
|
font-size: 12px;
|
|||
|
|
margin-top: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-preview {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-item {
|
|||
|
|
position: relative;
|
|||
|
|
width: 80px;
|
|||
|
|
height: 80px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid #d9d9d9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-image {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-remove {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 4px;
|
|||
|
|
right: 4px;
|
|||
|
|
background: rgba(0,0,0,0.5);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 12px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.modal-footer {
|
|||
|
|
padding: 16px 24px;
|
|||
|
|
border-top: 1px solid #f0f0f0;
|
|||
|
|
background: #fafafa;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
padding: 10px 20px;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
font-weight: 500;
|
|||
|
|
min-width: 80px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary {
|
|||
|
|
background-color: #1890ff;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary:hover:not(:disabled) {
|
|||
|
|
background-color: #40a9ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-secondary {
|
|||
|
|
background-color: #f5f5f5;
|
|||
|
|
color: #666;
|
|||
|
|
border: 1px solid #d9d9d9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-secondary:hover:not(:disabled) {
|
|||
|
|
border-color: #40a9ff;
|
|||
|
|
color: #1890ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn:disabled {
|
|||
|
|
background-color: #f5f5f5;
|
|||
|
|
color: #d9d9d9;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
border-color: #f5f5f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab-pane {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tab-pane.active {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-row {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.checkbox-group {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-top: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.checkbox {
|
|||
|
|
width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.number-input-group {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.unit-text {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.select-wrapper {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.select-wrapper::after {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
right: 12px;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translateY(-50%);
|
|||
|
|
width: 0;
|
|||
|
|
height: 0;
|
|||
|
|
border-left: 4px solid transparent;
|
|||
|
|
border-right: 4px solid transparent;
|
|||
|
|
border-top: 4px solid #999;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 规格配置弹窗样式 */
|
|||
|
|
.spec-modal {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
z-index: 1000;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-modal-overlay {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-modal-content {
|
|||
|
|
position: relative;
|
|||
|
|
width: 90%;
|
|||
|
|
max-width: 1200px;
|
|||
|
|
max-height: 90vh;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-modal-header {
|
|||
|
|
padding: 20px 24px;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
background: #fafafa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-modal-title {
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-modal-body {
|
|||
|
|
padding: 24px;
|
|||
|
|
max-height: calc(90vh - 120px);
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-combinations {
|
|||
|
|
margin-top: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-config-table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-config-table th,
|
|||
|
|
.spec-config-table td {
|
|||
|
|
padding: 12px 8px;
|
|||
|
|
text-align: left;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-config-table th {
|
|||
|
|
background-color: #fafafa;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-config-table td {
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-attr-input {
|
|||
|
|
width: 120px;
|
|||
|
|
padding: 6px 8px;
|
|||
|
|
border: 1px solid #d9d9d9;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-attr-input:focus {
|
|||
|
|
outline: none;
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-values-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 8px;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-value-tag {
|
|||
|
|
display: inline-block;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
background-color: #f0f0f0;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-value-remove {
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: #ff4d4f;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
padding: 0 4px;
|
|||
|
|
margin-left: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-value-input {
|
|||
|
|
width: 150px;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
border: 1px solid #d9d9d9;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-value-input:focus {
|
|||
|
|
outline: none;
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-sm {
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
min-width: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-table th,
|
|||
|
|
.spec-table td {
|
|||
|
|
padding: 12px 8px;
|
|||
|
|
text-align: center;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-table th {
|
|||
|
|
background-color: #fafafa;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-table td {
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-input {
|
|||
|
|
width: 80px;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
border: 1px solid #d9d9d9;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-input:focus {
|
|||
|
|
outline: none;
|
|||
|
|
border-color: #1890ff;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-danger {
|
|||
|
|
background-color: #ff4d4f;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-danger:hover {
|
|||
|
|
background-color: #ff7875;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-small {
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
min-width: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-modal-footer {
|
|||
|
|
padding: 16px 24px;
|
|||
|
|
border-top: 1px solid #f0f0f0;
|
|||
|
|
background: #fafafa;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 只读规格表格样式 */
|
|||
|
|
.spec-readonly-container {
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: #fafafa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-readonly-table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
font-size: 14px;
|
|||
|
|
background: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-readonly-table th,
|
|||
|
|
.spec-readonly-table td {
|
|||
|
|
padding: 10px 8px;
|
|||
|
|
text-align: center;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-readonly-table th {
|
|||
|
|
background-color: #fafafa;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-readonly-table td {
|
|||
|
|
color: #666;
|
|||
|
|
background: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spec-readonly-actions {
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
background: #fafafa;
|
|||
|
|
text-align: right;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<!-- 模态框头部 -->
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h2 class="modal-title">添加商品</h2>
|
|||
|
|
<button class="close-btn" onclick="closeModal()">×</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 模态框内容 -->
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<!-- 标签页导航 -->
|
|||
|
|
<div class="tabs-container">
|
|||
|
|
<div class="tabs">
|
|||
|
|
<div class="tab active" data-tab="basic">基础信息</div>
|
|||
|
|
<div class="tab" data-tab="sales">销售信息</div>
|
|||
|
|
<div class="tab" data-tab="other">其他信息</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 表单内容 -->
|
|||
|
|
<form id="addProductForm">
|
|||
|
|
<!-- 基础信息标签页 -->
|
|||
|
|
<div id="basic" class="tab-pane active">
|
|||
|
|
<div class="tab-content">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>商品图片
|
|||
|
|
</label>
|
|||
|
|
<div class="upload-area" id="uploadArea">
|
|||
|
|
<div class="upload-icon">📷</div>
|
|||
|
|
<div class="upload-text">
|
|||
|
|
将文件拖到此处,或<a href="#" class="upload-highlight">点击上传</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="upload-hint">只能上传jpg/png文件,且不超过500kb</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="image-preview" id="imagePreview"></div>
|
|||
|
|
<div class="error-message" id="imageError" style="display: none;">不能为空。</div>
|
|||
|
|
<input type="file" id="fileInput" accept="image/jpeg,image/png" multiple style="display: none;">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>商品类目
|
|||
|
|
</label>
|
|||
|
|
<div class="select-wrapper">
|
|||
|
|
<select class="form-select" id="category" required>
|
|||
|
|
<option value="">请选择商品类目</option>
|
|||
|
|
<option value="vegetables">蔬菜类</option>
|
|||
|
|
<option value="fruits">水果类</option>
|
|||
|
|
<option value="meat">肉类</option>
|
|||
|
|
<option value="seafood">海鲜类</option>
|
|||
|
|
<option value="grains">粮油类</option>
|
|||
|
|
<option value="dairy">乳制品</option>
|
|||
|
|
<option value="beverages">饮料类</option>
|
|||
|
|
<option value="snacks">零食类</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="error-message" id="categoryError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>商品名称
|
|||
|
|
</label>
|
|||
|
|
<input type="text" class="form-input" id="productName" placeholder="请输入商品名称" required>
|
|||
|
|
<div class="error-message" id="nameError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>商品描述
|
|||
|
|
</label>
|
|||
|
|
<textarea class="form-textarea" id="description" placeholder="请简单描述,如别称、口感、用途等" required></textarea>
|
|||
|
|
<div class="error-message" id="descriptionError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 销售信息标签页 -->
|
|||
|
|
<div id="sales" class="tab-pane">
|
|||
|
|
<div class="tab-content">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>销售单位
|
|||
|
|
</label>
|
|||
|
|
<div class="select-wrapper">
|
|||
|
|
<select class="form-select" id="salesUnit" required>
|
|||
|
|
<option value="">请选择销售单位</option>
|
|||
|
|
<option value="piece">个</option>
|
|||
|
|
<option value="kg">公斤</option>
|
|||
|
|
<option value="bag">袋</option>
|
|||
|
|
<option value="box">盒</option>
|
|||
|
|
<option value="bottle">瓶</option>
|
|||
|
|
<option value="pack">包</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="error-message" id="salesUnitError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>成本
|
|||
|
|
</label>
|
|||
|
|
<div class="number-input-group">
|
|||
|
|
<input type="number" class="form-input" id="costPrice" placeholder="请输入价格" min="0" step="0.01" required>
|
|||
|
|
<span class="unit-text">单位(元)</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="error-message" id="costPriceError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>市场价
|
|||
|
|
</label>
|
|||
|
|
<div class="number-input-group">
|
|||
|
|
<input type="number" class="form-input" id="marketPrice" placeholder="请输入市场价" min="0" step="0.01" required>
|
|||
|
|
<span class="unit-text">单位(元)</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="error-message" id="marketPriceError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>库存
|
|||
|
|
</label>
|
|||
|
|
<input type="number" class="form-input" id="stock" placeholder="请输入库存" min="0" required>
|
|||
|
|
<div class="error-message" id="stockError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>重量
|
|||
|
|
</label>
|
|||
|
|
<div class="number-input-group">
|
|||
|
|
<input type="number" class="form-input" id="weight" placeholder="请输入重量" min="0" step="0.01" required>
|
|||
|
|
<span class="unit-text">KG</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="error-message" id="weightError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">
|
|||
|
|
<span class="required">*</span>体积
|
|||
|
|
</label>
|
|||
|
|
<div class="number-input-group">
|
|||
|
|
<input type="number" class="form-input" id="volume" placeholder="请输入体积" min="0" step="0.01" required>
|
|||
|
|
<span class="unit-text">立方米</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="error-message" id="volumeError" style="display: none;">不能为空。</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 规格配置显示区域 -->
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">规格配置</label>
|
|||
|
|
|
|||
|
|
<!-- 无规格配置时显示 -->
|
|||
|
|
<div id="noSpecGroup">
|
|||
|
|
<button type="button" class="btn btn-secondary" onclick="configureSpecs()">添加多规格配置</button>
|
|||
|
|
<div style="margin-top: 8px; color: #999; font-size: 12px;">单规格商品无需配置</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 有规格配置时显示 -->
|
|||
|
|
<div id="specDisplayGroup" style="display: none;">
|
|||
|
|
<div class="spec-readonly-container">
|
|||
|
|
<table class="spec-readonly-table" id="specReadonlyTable">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>序号</th>
|
|||
|
|
<th>属性</th>
|
|||
|
|
<th>成本(元)</th>
|
|||
|
|
<th>市场价(元)</th>
|
|||
|
|
<th>库存</th>
|
|||
|
|
<th>重量(千克)</th>
|
|||
|
|
<th>体积(立方米)</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="specReadonlyTableBody">
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
<div class="spec-readonly-actions">
|
|||
|
|
<button type="button" class="btn btn-secondary" onclick="configureSpecs()">修改规格配置</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 其他信息标签页 -->
|
|||
|
|
<div id="other" class="tab-pane">
|
|||
|
|
<div class="tab-content">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">产地</label>
|
|||
|
|
<input type="text" class="form-input" id="origin" placeholder="请选择">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">保质期</label>
|
|||
|
|
<div class="select-wrapper">
|
|||
|
|
<select class="form-select" id="shelfLife">
|
|||
|
|
<option value="">请填写保质期</option>
|
|||
|
|
<option value="1day">1天</option>
|
|||
|
|
<option value="3days">3天</option>
|
|||
|
|
<option value="1week">1周</option>
|
|||
|
|
<option value="2weeks">2周</option>
|
|||
|
|
<option value="1month">1个月</option>
|
|||
|
|
<option value="3months">3个月</option>
|
|||
|
|
<option value="6months">6个月</option>
|
|||
|
|
<option value="1year">1年</option>
|
|||
|
|
<option value="2years">2年</option>
|
|||
|
|
<option value="custom">自定义</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group" id="customShelfLifeGroup" style="display: none;">
|
|||
|
|
<label class="form-label">自定义保质期</label>
|
|||
|
|
<input type="text" class="form-input" id="customShelfLife" placeholder="请输入保质期">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">商品介绍图</label>
|
|||
|
|
<div class="upload-area" id="productIntroUploadArea">
|
|||
|
|
<div class="upload-icon">📷</div>
|
|||
|
|
<div class="upload-text">
|
|||
|
|
将文件拖到此处,或<a href="#" class="upload-highlight">点击上传</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="upload-hint">支持mp4、avi、mov等视频格式,建议不超过50MB</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="image-preview" id="productIntroPreview"></div>
|
|||
|
|
<input type="file" id="productIntroFileInput" accept="video/*,image/*" multiple style="display: none;">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">商品状态</label>
|
|||
|
|
<div class="select-wrapper">
|
|||
|
|
<select class="form-select" id="status">
|
|||
|
|
<option value="warehouse">仓库中</option>
|
|||
|
|
<option value="online">立即上架</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">商品标签</label>
|
|||
|
|
<input type="text" class="form-input" id="tags" placeholder="多个标签用逗号分隔,如:新鲜,有机,精选">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">备注信息</label>
|
|||
|
|
<textarea class="form-textarea" id="remarks" placeholder="商品备注信息"></textarea>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">排序权重</label>
|
|||
|
|
<div class="number-input-group">
|
|||
|
|
<input type="number" class="form-input" id="sortOrder" placeholder="0" value="0">
|
|||
|
|
<span class="unit-text">数值越大排序越靠前</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 模态框底部 -->
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" onclick="resetForm()">重置</button>
|
|||
|
|
<button type="button" class="btn btn-primary" id="nextBtn" onclick="nextStep()">下一步</button>
|
|||
|
|
<button type="button" class="btn btn-primary" id="saveBtn" onclick="saveProduct()" style="display: none;">保存并放入仓库</button>
|
|||
|
|
<button type="button" class="btn btn-primary" id="publishBtn" onclick="publishProduct()" style="display: none;">保存并上架</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 规格配置弹窗 -->
|
|||
|
|
<div id="specModal" class="spec-modal" style="display: none;">
|
|||
|
|
<div class="spec-modal-overlay" onclick="closeSpecModal()"></div>
|
|||
|
|
<div class="spec-modal-content">
|
|||
|
|
<div class="spec-modal-header">
|
|||
|
|
<h3 class="spec-modal-title">添加属性</h3>
|
|||
|
|
<button class="close-btn" onclick="closeSpecModal()">×</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="spec-modal-body">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label class="form-label">属性配置</label>
|
|||
|
|
<table class="spec-config-table" id="specConfigTable">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>属性名</th>
|
|||
|
|
<th>属性值</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr>
|
|||
|
|
<td><input type="text" class="spec-attr-input" value="温度" data-attr="temperature"></td>
|
|||
|
|
<td>
|
|||
|
|
<div class="spec-values-container" data-attr="temperature">
|
|||
|
|
<span class="spec-value-tag">热 <button type="button" class="spec-value-remove" onclick="removeSpecValue('temperature', '热')">×</button></span>
|
|||
|
|
<span class="spec-value-tag">冷 <button type="button" class="spec-value-remove" onclick="removeSpecValue('temperature', '冷')">×</button></span>
|
|||
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="addSpecValue('temperature')">+增加属性值</button>
|
|||
|
|
<input type="text" class="spec-value-input" placeholder="输入属性值按回车确认" style="display: none;" onkeypress="handleSpecValueInput(event, 'temperature')">
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeSpecAttribute('temperature')">删除</button></td>
|
|||
|
|
</tr>
|
|||
|
|
<tr>
|
|||
|
|
<td><input type="text" class="spec-attr-input" value="体积" data-attr="volume"></td>
|
|||
|
|
<td>
|
|||
|
|
<div class="spec-values-container" data-attr="volume">
|
|||
|
|
<span class="spec-value-tag">大 <button type="button" class="spec-value-remove" onclick="removeSpecValue('volume', '大')">×</button></span>
|
|||
|
|
<span class="spec-value-tag">小 <button type="button" class="spec-value-remove" onclick="removeSpecValue('volume', '小')">×</button></span>
|
|||
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="addSpecValue('volume')">+增加属性值</button>
|
|||
|
|
<input type="text" class="spec-value-input" placeholder="输入属性值按回车确认" style="display: none;" onkeypress="handleSpecValueInput(event, 'volume')">
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeSpecAttribute('volume')">删除</button></td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
<div style="margin-top: 10px;">
|
|||
|
|
<button type="button" class="btn btn-secondary" onclick="addSpecAttribute()">+添加属性</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 规格组合表格 -->
|
|||
|
|
<div class="spec-combinations">
|
|||
|
|
<table class="spec-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>序号</th>
|
|||
|
|
<th>属性</th>
|
|||
|
|
<th>成本(元)</th>
|
|||
|
|
<th>市场价(元)</th>
|
|||
|
|
<th>库存</th>
|
|||
|
|
<th>重量(千克)</th>
|
|||
|
|
<th>体积(立方米)</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="specCombinationsTable">
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="spec-modal-footer">
|
|||
|
|
<button type="button" class="btn btn-secondary" onclick="closeSpecModal()">取消</button>
|
|||
|
|
<button type="button" class="btn btn-primary" onclick="saveSpecConfig()">确定</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// 标签页切换
|
|||
|
|
const tabs = document.querySelectorAll('.tab');
|
|||
|
|
const tabPanes = document.querySelectorAll('.tab-pane');
|
|||
|
|
let currentTabIndex = 0;
|
|||
|
|
|
|||
|
|
tabs.forEach((tab, index) => {
|
|||
|
|
tab.addEventListener('click', () => {
|
|||
|
|
switchTab(index);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function switchTab(index) {
|
|||
|
|
// 移除所有活动状态
|
|||
|
|
tabs.forEach(tab => tab.classList.remove('active'));
|
|||
|
|
tabPanes.forEach(pane => pane.classList.remove('active'));
|
|||
|
|
|
|||
|
|
// 激活当前标签页
|
|||
|
|
tabs[index].classList.add('active');
|
|||
|
|
tabPanes[index].classList.add('active');
|
|||
|
|
currentTabIndex = index;
|
|||
|
|
|
|||
|
|
// 更新按钮显示
|
|||
|
|
updateButtons();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateButtons() {
|
|||
|
|
const nextBtn = document.getElementById('nextBtn');
|
|||
|
|
const saveBtn = document.getElementById('saveBtn');
|
|||
|
|
const publishBtn = document.getElementById('publishBtn');
|
|||
|
|
|
|||
|
|
if (currentTabIndex < 2) {
|
|||
|
|
nextBtn.style.display = 'block';
|
|||
|
|
saveBtn.style.display = 'none';
|
|||
|
|
publishBtn.style.display = 'none';
|
|||
|
|
} else {
|
|||
|
|
nextBtn.style.display = 'none';
|
|||
|
|
saveBtn.style.display = 'block';
|
|||
|
|
publishBtn.style.display = 'block';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function nextStep() {
|
|||
|
|
if (validateCurrentTab()) {
|
|||
|
|
if (currentTabIndex < 2) {
|
|||
|
|
switchTab(currentTabIndex + 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function validateCurrentTab() {
|
|||
|
|
let isValid = true;
|
|||
|
|
const currentPane = tabPanes[currentTabIndex];
|
|||
|
|
const requiredFields = currentPane.querySelectorAll('[required]');
|
|||
|
|
|
|||
|
|
requiredFields.forEach(field => {
|
|||
|
|
const errorElement = document.getElementById(field.id + 'Error');
|
|||
|
|
if (!field.value.trim()) {
|
|||
|
|
if (errorElement) {
|
|||
|
|
errorElement.style.display = 'block';
|
|||
|
|
}
|
|||
|
|
isValid = false;
|
|||
|
|
} else {
|
|||
|
|
if (errorElement) {
|
|||
|
|
errorElement.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 特殊验证:图片上传
|
|||
|
|
if (currentTabIndex === 0) {
|
|||
|
|
const imagePreview = document.getElementById('imagePreview');
|
|||
|
|
const imageError = document.getElementById('imageError');
|
|||
|
|
if (imagePreview.children.length === 0) {
|
|||
|
|
imageError.style.display = 'block';
|
|||
|
|
isValid = false;
|
|||
|
|
} else {
|
|||
|
|
imageError.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return isValid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 文件上传功能
|
|||
|
|
const uploadArea = document.getElementById('uploadArea');
|
|||
|
|
const fileInput = document.getElementById('fileInput');
|
|||
|
|
const imagePreview = document.getElementById('imagePreview');
|
|||
|
|
|
|||
|
|
uploadArea.addEventListener('click', () => {
|
|||
|
|
fileInput.click();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
uploadArea.addEventListener('dragover', (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
uploadArea.classList.add('dragover');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
uploadArea.addEventListener('dragleave', () => {
|
|||
|
|
uploadArea.classList.remove('dragover');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
uploadArea.addEventListener('drop', (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
uploadArea.classList.remove('dragover');
|
|||
|
|
const files = e.dataTransfer.files;
|
|||
|
|
handleFiles(files);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
fileInput.addEventListener('change', (e) => {
|
|||
|
|
handleFiles(e.target.files);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function handleFiles(files) {
|
|||
|
|
Array.from(files).forEach(file => {
|
|||
|
|
if (file.type.startsWith('image/') && file.size <= 500 * 1024) {
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
addImagePreview(e.target.result, file.name);
|
|||
|
|
};
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
} else {
|
|||
|
|
alert('请选择小于500KB的图片文件');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addImagePreview(src, name) {
|
|||
|
|
const previewItem = document.createElement('div');
|
|||
|
|
previewItem.className = 'preview-item';
|
|||
|
|
previewItem.innerHTML = `
|
|||
|
|
<img src="${src}" alt="${name}" class="preview-image">
|
|||
|
|
<button type="button" class="preview-remove" onclick="removeImage(this)">×</button>
|
|||
|
|
`;
|
|||
|
|
imagePreview.appendChild(previewItem);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeImage(button) {
|
|||
|
|
button.parentElement.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 保质期自定义选项
|
|||
|
|
const shelfLifeSelect = document.getElementById('shelfLife');
|
|||
|
|
const customShelfLifeGroup = document.getElementById('customShelfLifeGroup');
|
|||
|
|
|
|||
|
|
shelfLifeSelect.addEventListener('change', () => {
|
|||
|
|
customShelfLifeGroup.style.display = shelfLifeSelect.value === 'custom' ? 'block' : 'none';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 商品介绍图上传功能
|
|||
|
|
const productIntroUploadArea = document.getElementById('productIntroUploadArea');
|
|||
|
|
const productIntroFileInput = document.getElementById('productIntroFileInput');
|
|||
|
|
const productIntroPreview = document.getElementById('productIntroPreview');
|
|||
|
|
|
|||
|
|
productIntroUploadArea.addEventListener('click', () => {
|
|||
|
|
productIntroFileInput.click();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
productIntroUploadArea.addEventListener('dragover', (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
productIntroUploadArea.classList.add('dragover');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
productIntroUploadArea.addEventListener('dragleave', () => {
|
|||
|
|
productIntroUploadArea.classList.remove('dragover');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
productIntroUploadArea.addEventListener('drop', (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
productIntroUploadArea.classList.remove('dragover');
|
|||
|
|
const files = e.dataTransfer.files;
|
|||
|
|
handleProductIntroFiles(files);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
productIntroFileInput.addEventListener('change', (e) => {
|
|||
|
|
handleProductIntroFiles(e.target.files);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function handleProductIntroFiles(files) {
|
|||
|
|
Array.from(files).forEach(file => {
|
|||
|
|
if ((file.type.startsWith('image/') || file.type.startsWith('video/')) && file.size <= 50 * 1024 * 1024) {
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
addProductIntroPreview(e.target.result, file.name, file.type);
|
|||
|
|
};
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
} else {
|
|||
|
|
alert('请选择小于50MB的图片或视频文件');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addProductIntroPreview(src, name, type) {
|
|||
|
|
const previewItem = document.createElement('div');
|
|||
|
|
previewItem.className = 'preview-item';
|
|||
|
|
|
|||
|
|
if (type.startsWith('video/')) {
|
|||
|
|
previewItem.innerHTML = `
|
|||
|
|
<video src="${src}" class="preview-image" controls></video>
|
|||
|
|
<button type="button" class="preview-remove" onclick="removeProductIntroMedia(this)">×</button>
|
|||
|
|
`;
|
|||
|
|
} else {
|
|||
|
|
previewItem.innerHTML = `
|
|||
|
|
<img src="${src}" alt="${name}" class="preview-image">
|
|||
|
|
<button type="button" class="preview-remove" onclick="removeProductIntroMedia(this)">×</button>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
productIntroPreview.appendChild(previewItem);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeProductIntroMedia(button) {
|
|||
|
|
button.parentElement.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 规格配置功能
|
|||
|
|
function configureSpecs() {
|
|||
|
|
document.getElementById('specModal').style.display = 'flex';
|
|||
|
|
// 初始化现有属性值的显示
|
|||
|
|
updateSpecValuesDisplay('temperature');
|
|||
|
|
updateSpecValuesDisplay('volume');
|
|||
|
|
// 初始化规格表格
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeSpecModal() {
|
|||
|
|
document.getElementById('specModal').style.display = 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 规格配置数据管理
|
|||
|
|
let specData = {
|
|||
|
|
temperature: ['热', '冷'],
|
|||
|
|
volume: ['大', '小']
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 删除属性
|
|||
|
|
function deleteAttribute(attributeName) {
|
|||
|
|
if (confirm(`确定要删除${attributeName === 'temperature' ? '温度' : '体积'}属性吗?`)) {
|
|||
|
|
delete specData[attributeName];
|
|||
|
|
updateSpecModal();
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除规格选项
|
|||
|
|
function removeOption(attributeName, index) {
|
|||
|
|
specData[attributeName].splice(index, 1);
|
|||
|
|
if (specData[attributeName].length === 0) {
|
|||
|
|
delete specData[attributeName];
|
|||
|
|
}
|
|||
|
|
updateSpecModal();
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加规格选项
|
|||
|
|
function addOption(attributeName) {
|
|||
|
|
const optionName = prompt(`请输入新的${attributeName === 'temperature' ? '温度' : '体积'}规格:`);
|
|||
|
|
if (optionName && optionName.trim()) {
|
|||
|
|
specData[attributeName].push(optionName.trim());
|
|||
|
|
updateSpecModal();
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加新属性
|
|||
|
|
function addSpecAttribute() {
|
|||
|
|
const tableBody = document.querySelector('#specConfigTable tbody');
|
|||
|
|
const attributeKey = 'attribute' + Date.now(); // 使用时间戳作为唯一标识
|
|||
|
|
|
|||
|
|
const newRow = document.createElement('tr');
|
|||
|
|
newRow.innerHTML = `
|
|||
|
|
<td><input type="text" class="spec-attr-input" value="新属性" data-attr="${attributeKey}"></td>
|
|||
|
|
<td>
|
|||
|
|
<div class="spec-values-container" data-attr="${attributeKey}">
|
|||
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="addSpecValue('${attributeKey}')">+增加属性值</button>
|
|||
|
|
<input type="text" class="spec-value-input" placeholder="输入属性值按回车确认" style="display: none;" onkeypress="handleSpecValueInput(event, '${attributeKey}')">
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeSpecAttribute('${attributeKey}')">删除</button></td>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
tableBody.appendChild(newRow);
|
|||
|
|
|
|||
|
|
// 初始化新属性的数据
|
|||
|
|
specData[attributeKey] = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 增加属性值
|
|||
|
|
function addSpecValue(attributeKey) {
|
|||
|
|
const container = document.querySelector(`.spec-values-container[data-attr="${attributeKey}"]`);
|
|||
|
|
const input = container.querySelector('.spec-value-input');
|
|||
|
|
const addButton = container.querySelector('.btn-sm');
|
|||
|
|
|
|||
|
|
// 隐藏按钮,显示输入框
|
|||
|
|
addButton.style.display = 'none';
|
|||
|
|
input.style.display = 'inline-block';
|
|||
|
|
input.focus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理属性值输入
|
|||
|
|
function handleSpecValueInput(event, attributeKey) {
|
|||
|
|
if (event.key === 'Enter') {
|
|||
|
|
const inputValue = event.target.value.trim();
|
|||
|
|
if (inputValue) {
|
|||
|
|
// 添加属性值到数据
|
|||
|
|
if (!specData[attributeKey]) {
|
|||
|
|
specData[attributeKey] = [];
|
|||
|
|
}
|
|||
|
|
specData[attributeKey].push(inputValue);
|
|||
|
|
|
|||
|
|
// 更新显示
|
|||
|
|
updateSpecValuesDisplay(attributeKey);
|
|||
|
|
|
|||
|
|
// 清空输入框并重新显示按钮
|
|||
|
|
event.target.value = '';
|
|||
|
|
event.target.style.display = 'none';
|
|||
|
|
const addButton = event.target.parentNode.querySelector('.btn-sm');
|
|||
|
|
addButton.style.display = 'inline-block';
|
|||
|
|
|
|||
|
|
// 重新生成规格组合表格
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新属性值显示
|
|||
|
|
function updateSpecValuesDisplay(attributeKey) {
|
|||
|
|
const container = document.querySelector(`.spec-values-container[data-attr="${attributeKey}"]`);
|
|||
|
|
if (!container) return;
|
|||
|
|
|
|||
|
|
// 清空容器(保留输入框和按钮)
|
|||
|
|
const input = container.querySelector('.spec-value-input');
|
|||
|
|
const addButton = container.querySelector('.btn-sm');
|
|||
|
|
container.innerHTML = '';
|
|||
|
|
container.appendChild(addButton);
|
|||
|
|
container.appendChild(input);
|
|||
|
|
|
|||
|
|
// 添加所有属性值标签
|
|||
|
|
if (specData[attributeKey]) {
|
|||
|
|
specData[attributeKey].forEach(value => {
|
|||
|
|
const tag = document.createElement('span');
|
|||
|
|
tag.className = 'spec-value-tag';
|
|||
|
|
tag.innerHTML = `${value} <button type="button" class="spec-value-remove" onclick="removeSpecValue('${attributeKey}', '${value}')">×</button>`;
|
|||
|
|
container.insertBefore(tag, addButton);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除属性值
|
|||
|
|
function removeSpecValue(attributeKey, value) {
|
|||
|
|
if (specData[attributeKey]) {
|
|||
|
|
const index = specData[attributeKey].indexOf(value);
|
|||
|
|
if (index > -1) {
|
|||
|
|
specData[attributeKey].splice(index, 1);
|
|||
|
|
updateSpecValuesDisplay(attributeKey);
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除整个属性
|
|||
|
|
function removeSpecAttribute(attributeKey) {
|
|||
|
|
// 删除数据
|
|||
|
|
delete specData[attributeKey];
|
|||
|
|
|
|||
|
|
// 删除表格行
|
|||
|
|
const row = document.querySelector(`input[data-attr="${attributeKey}"]`).closest('tr');
|
|||
|
|
row.remove();
|
|||
|
|
|
|||
|
|
// 重新生成规格表格
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听属性名称变化
|
|||
|
|
function setupAttributeNameListener() {
|
|||
|
|
document.addEventListener('input', function(event) {
|
|||
|
|
if (event.target.classList.contains('spec-attr-input')) {
|
|||
|
|
// 属性名称变化时重新生成规格表格
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成所有规格组合
|
|||
|
|
function generateCombinations(attributes) {
|
|||
|
|
const keys = Object.keys(attributes);
|
|||
|
|
if (keys.length === 0) return [];
|
|||
|
|
|
|||
|
|
function combine(arrays) {
|
|||
|
|
if (arrays.length === 1) return arrays[0].map(item => [item]);
|
|||
|
|
|
|||
|
|
const result = [];
|
|||
|
|
const firstArray = arrays[0];
|
|||
|
|
const remainingCombinations = combine(arrays.slice(1));
|
|||
|
|
|
|||
|
|
for (const item of firstArray) {
|
|||
|
|
for (const combination of remainingCombinations) {
|
|||
|
|
result.push([item, ...combination]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const attributeArrays = keys.map(key => attributes[key]);
|
|||
|
|
return combine(attributeArrays);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重新生成规格表格
|
|||
|
|
function regenerateSpecTable() {
|
|||
|
|
// 获取实际的属性数据
|
|||
|
|
const actualSpecData = {};
|
|||
|
|
const rows = document.querySelectorAll('#specConfigTable tbody tr');
|
|||
|
|
|
|||
|
|
rows.forEach(row => {
|
|||
|
|
const attrInput = row.querySelector('.spec-attr-input');
|
|||
|
|
const attrKey = attrInput.getAttribute('data-attr');
|
|||
|
|
const attrName = attrInput.value.trim();
|
|||
|
|
|
|||
|
|
if (attrName && specData[attrKey] && specData[attrKey].length > 0) {
|
|||
|
|
actualSpecData[attrName] = [...specData[attrKey]];
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const combinations = generateCombinations(actualSpecData);
|
|||
|
|
const tableBody = document.getElementById('specCombinationsTable');
|
|||
|
|
|
|||
|
|
if (!tableBody) return;
|
|||
|
|
|
|||
|
|
tableBody.innerHTML = combinations.map((combination, index) => `
|
|||
|
|
<tr>
|
|||
|
|
<td>${index + 1}</td>
|
|||
|
|
<td>${combination.join('')}</td>
|
|||
|
|
<td><input type="number" class="spec-input" value="0" min="0" step="0.01"></td>
|
|||
|
|
<td><input type="number" class="spec-input" value="0" min="0" step="0.01"></td>
|
|||
|
|
<td><input type="number" class="spec-input" value="0" min="0"></td>
|
|||
|
|
<td><input type="number" class="spec-input" value="0" min="0" step="0.01"></td>
|
|||
|
|
<td><input type="number" class="spec-input" value="0" min="0" step="0.01"></td>
|
|||
|
|
<td><button type="button" class="btn btn-danger btn-small" onclick="deleteSpecRow(this)">删除</button></td>
|
|||
|
|
</tr>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除规格行
|
|||
|
|
function deleteSpecRow(button) {
|
|||
|
|
button.closest('tr').remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存规格配置
|
|||
|
|
function saveSpecConfig() {
|
|||
|
|
const tableRows = document.querySelectorAll('#specCombinationsTable tr');
|
|||
|
|
const specConfigs = [];
|
|||
|
|
|
|||
|
|
tableRows.forEach(row => {
|
|||
|
|
const inputs = row.querySelectorAll('.spec-input');
|
|||
|
|
const spec = row.cells[1].textContent;
|
|||
|
|
specConfigs.push({
|
|||
|
|
spec: spec,
|
|||
|
|
cost: parseFloat(inputs[0].value) || 0,
|
|||
|
|
marketPrice: parseFloat(inputs[1].value) || 0,
|
|||
|
|
stock: parseInt(inputs[2].value) || 0,
|
|||
|
|
weight: parseFloat(inputs[3].value) || 0,
|
|||
|
|
volume: parseFloat(inputs[4].value) || 0
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 更新销售信息中的只读表格
|
|||
|
|
updateReadonlySpecTable(specConfigs);
|
|||
|
|
|
|||
|
|
console.log('规格配置已保存:', specConfigs);
|
|||
|
|
alert('规格配置已保存');
|
|||
|
|
closeSpecModal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新销售信息中的只读规格表格
|
|||
|
|
function updateReadonlySpecTable(specConfigs) {
|
|||
|
|
const noSpecGroup = document.getElementById('noSpecGroup');
|
|||
|
|
const specDisplayGroup = document.getElementById('specDisplayGroup');
|
|||
|
|
const tableBody = document.getElementById('specReadonlyTableBody');
|
|||
|
|
|
|||
|
|
if (specConfigs && specConfigs.length > 0) {
|
|||
|
|
// 隐藏无规格区域,显示规格配置区域
|
|||
|
|
noSpecGroup.style.display = 'none';
|
|||
|
|
specDisplayGroup.style.display = 'block';
|
|||
|
|
|
|||
|
|
// 更新表格内容
|
|||
|
|
tableBody.innerHTML = specConfigs.map((config, index) => `
|
|||
|
|
<tr>
|
|||
|
|
<td>${index + 1}</td>
|
|||
|
|
<td>${config.spec}</td>
|
|||
|
|
<td>${config.cost.toFixed(2)}</td>
|
|||
|
|
<td>${config.marketPrice.toFixed(2)}</td>
|
|||
|
|
<td>${config.stock}</td>
|
|||
|
|
<td>${config.weight.toFixed(2)}</td>
|
|||
|
|
<td>${config.volume.toFixed(2)}</td>
|
|||
|
|
</tr>
|
|||
|
|
`).join('');
|
|||
|
|
} else {
|
|||
|
|
// 显示无规格区域,隐藏规格配置区域
|
|||
|
|
noSpecGroup.style.display = 'block';
|
|||
|
|
specDisplayGroup.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否有规格配置并更新显示
|
|||
|
|
function checkAndUpdateSpecDisplay() {
|
|||
|
|
// 获取实际的属性数据
|
|||
|
|
const actualSpecData = {};
|
|||
|
|
const rows = document.querySelectorAll('#specConfigTable tbody tr');
|
|||
|
|
|
|||
|
|
rows.forEach(row => {
|
|||
|
|
const attrInput = row.querySelector('.spec-attr-input');
|
|||
|
|
const attrKey = attrInput.getAttribute('data-attr');
|
|||
|
|
const attrName = attrInput.value.trim();
|
|||
|
|
|
|||
|
|
if (attrName && specData[attrKey] && specData[attrKey].length > 0) {
|
|||
|
|
actualSpecData[attrName] = [...specData[attrKey]];
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const combinations = generateCombinations(actualSpecData);
|
|||
|
|
|
|||
|
|
if (combinations.length > 0) {
|
|||
|
|
const defaultConfigs = combinations.map((combination, index) => ({
|
|||
|
|
spec: combination.join(''),
|
|||
|
|
cost: 0,
|
|||
|
|
marketPrice: 0,
|
|||
|
|
stock: 0,
|
|||
|
|
weight: 0,
|
|||
|
|
volume: 0
|
|||
|
|
}));
|
|||
|
|
updateReadonlySpecTable(defaultConfigs);
|
|||
|
|
} else {
|
|||
|
|
updateReadonlySpecTable([]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 表单操作
|
|||
|
|
function resetForm() {
|
|||
|
|
document.getElementById('addProductForm').reset();
|
|||
|
|
imagePreview.innerHTML = '';
|
|||
|
|
switchTab(0);
|
|||
|
|
hideAllErrors();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hideAllErrors() {
|
|||
|
|
document.querySelectorAll('.error-message').forEach(error => {
|
|||
|
|
error.style.display = 'none';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveProduct() {
|
|||
|
|
if (validateAllTabs()) {
|
|||
|
|
alert('商品已保存到仓库');
|
|||
|
|
// 这里添加保存逻辑
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function publishProduct() {
|
|||
|
|
if (validateAllTabs()) {
|
|||
|
|
alert('商品已保存并上架');
|
|||
|
|
// 这里添加发布逻辑
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function validateAllTabs() {
|
|||
|
|
let allValid = true;
|
|||
|
|
for (let i = 0; i < 3; i++) {
|
|||
|
|
const originalIndex = currentTabIndex;
|
|||
|
|
currentTabIndex = i;
|
|||
|
|
if (!validateCurrentTab()) {
|
|||
|
|
allValid = false;
|
|||
|
|
if (originalIndex !== i) {
|
|||
|
|
switchTab(i);
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
currentTabIndex = originalIndex;
|
|||
|
|
}
|
|||
|
|
return allValid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
if (confirm('确定要关闭吗?未保存的数据将丢失。')) {
|
|||
|
|
// 关闭模态框逻辑
|
|||
|
|
window.history.back();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
updateButtons();
|
|||
|
|
|
|||
|
|
// 初始化规格表格
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
regenerateSpecTable();
|
|||
|
|
setupAttributeNameListener();
|
|||
|
|
checkAndUpdateSpecDisplay();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|