652 lines
28 KiB
HTML
652 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html xmlns:th="http://www.thymeleaf.org">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>商品编辑 - 商家后台</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
</head>
|
||
<body>
|
||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
|
||
<div class="navbar-nav ms-auto">
|
||
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
|
||
<a class="nav-link active" href="/merchant/products.html">商品管理</a>
|
||
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
|
||
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
|
||
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
|
||
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
|
||
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
|
||
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
|
||
<a class="nav-link" href="/merchant/users.html">用户管理</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container mt-4">
|
||
<h2 id="pageTitle">新增商品</h2>
|
||
|
||
<form id="productForm" enctype="multipart/form-data">
|
||
<input type="hidden" id="productId">
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header">基本信息</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label for="name" class="form-label">商品名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="name" required>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="category" class="form-label">分类</label>
|
||
<input type="text" class="form-control" id="category" placeholder="例如:奶茶、果茶">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="description" class="form-label">商品描述</label>
|
||
<textarea class="form-control" id="description" rows="3"></textarea>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="basePrice" class="form-label">基础价格 <span class="text-danger">*</span></label>
|
||
<input type="number" class="form-control" id="basePrice" step="0.01" min="0" required>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="image" class="form-label">商品图片</label>
|
||
<input type="file" class="form-control" id="image" accept="image/*">
|
||
<div id="imagePreview" class="mt-2"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span>规格管理</span>
|
||
<button type="button" class="btn btn-sm btn-primary" onclick="addSpec()">添加规格</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="specsList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span>配料管理</span>
|
||
<button type="button" class="btn btn-sm btn-primary" onclick="addTopping()">添加配料</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="toppingsList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span>定制选项管理(甜度、冰度)</span>
|
||
<div>
|
||
<button type="button" class="btn btn-sm btn-primary" onclick="addCustom('sweetness')">添加甜度</button>
|
||
<button type="button" class="btn btn-sm btn-primary" onclick="addCustom('ice')">添加冰度</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<h6>甜度选项</h6>
|
||
<div id="sweetnessList"></div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<h6>冰度选项</h6>
|
||
<div id="iceList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
<a href="/merchant/products.html" class="btn btn-secondary">取消</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script src="/static/js/sidebar-replacer.js"></script>
|
||
<script src="/static/js/ui-enhancements.js"></script>
|
||
<script>
|
||
// 使用Session,浏览器自动发送Cookie
|
||
|
||
// 检查登录状态
|
||
async function checkLogin() {
|
||
try {
|
||
const response = await fetch('/merchant/profile', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
if (data.code !== 200 || !data.data) {
|
||
window.location.href = '/merchant/login.html';
|
||
return false;
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
window.location.href = '/merchant/login.html';
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const productId = urlParams.get('id');
|
||
let specs = [];
|
||
let toppings = [];
|
||
let customs = {
|
||
sweetness: [],
|
||
ice: []
|
||
};
|
||
|
||
// 页面加载
|
||
checkLogin().then(isLoggedIn => {
|
||
if (isLoggedIn) {
|
||
if (productId) {
|
||
document.getElementById('pageTitle').textContent = '编辑商品';
|
||
loadProduct();
|
||
} else {
|
||
// 新商品时初始化默认选项
|
||
if (customs.sweetness.length === 0) {
|
||
customs.sweetness = [
|
||
{ optionType: 'sweetness', optionValue: '无糖', priceAdjust: 0 },
|
||
{ optionType: 'sweetness', optionValue: '少糖', priceAdjust: 0 },
|
||
{ optionType: 'sweetness', optionValue: '正常', priceAdjust: 0 },
|
||
{ optionType: 'sweetness', optionValue: '多糖', priceAdjust: 0 }
|
||
];
|
||
}
|
||
if (customs.ice.length === 0) {
|
||
customs.ice = [
|
||
{ optionType: 'ice', optionValue: '去冰', priceAdjust: 0 },
|
||
{ optionType: 'ice', optionValue: '少冰', priceAdjust: 0 },
|
||
{ optionType: 'ice', optionValue: '正常', priceAdjust: 0 },
|
||
{ optionType: 'ice', optionValue: '多冰', priceAdjust: 0 }
|
||
];
|
||
}
|
||
renderCustoms();
|
||
}
|
||
}
|
||
});
|
||
|
||
// 图片预览
|
||
document.getElementById('image').addEventListener('change', function(e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
document.getElementById('imagePreview').innerHTML =
|
||
`<img src="${e.target.result}" class="img-thumbnail" style="max-width: 200px;">`;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
|
||
// 加载商品信息
|
||
async function loadProduct() {
|
||
try {
|
||
const response = await fetch(`/merchant/products/${productId}`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.code === 200) {
|
||
const product = data.data.product;
|
||
document.getElementById('productId').value = product.id;
|
||
document.getElementById('name').value = product.name || '';
|
||
document.getElementById('category').value = product.category || '';
|
||
document.getElementById('description').value = product.description || '';
|
||
document.getElementById('basePrice').value = product.basePrice || '';
|
||
|
||
if (product.image) {
|
||
document.getElementById('imagePreview').innerHTML =
|
||
`<img src="${product.image}" class="img-thumbnail" style="max-width: 200px;">`;
|
||
}
|
||
|
||
specs = data.data.specs || [];
|
||
toppings = data.data.toppings || [];
|
||
const allCustoms = data.data.customs || [];
|
||
|
||
// 分离甜度和冰度选项
|
||
customs.sweetness = allCustoms.filter(c => c.optionType === 'sweetness');
|
||
customs.ice = allCustoms.filter(c => c.optionType === 'ice');
|
||
|
||
// 如果没有甜度选项,创建默认选项
|
||
if (customs.sweetness.length === 0) {
|
||
customs.sweetness = [
|
||
{ optionType: 'sweetness', optionValue: '无糖', priceAdjust: 0 },
|
||
{ optionType: 'sweetness', optionValue: '少糖', priceAdjust: 0 },
|
||
{ optionType: 'sweetness', optionValue: '正常', priceAdjust: 0 },
|
||
{ optionType: 'sweetness', optionValue: '多糖', priceAdjust: 0 }
|
||
];
|
||
}
|
||
|
||
// 如果没有冰度选项,创建默认选项
|
||
if (customs.ice.length === 0) {
|
||
customs.ice = [
|
||
{ optionType: 'ice', optionValue: '去冰', priceAdjust: 0 },
|
||
{ optionType: 'ice', optionValue: '少冰', priceAdjust: 0 },
|
||
{ optionType: 'ice', optionValue: '正常', priceAdjust: 0 },
|
||
{ optionType: 'ice', optionValue: '多冰', priceAdjust: 0 }
|
||
];
|
||
}
|
||
|
||
renderSpecs();
|
||
renderToppings();
|
||
renderCustoms();
|
||
} else {
|
||
if (data.message && data.message.includes('登录')) {
|
||
alert('请先登录');
|
||
window.location.href = '/merchant/login.html';
|
||
} else {
|
||
alert(data.message || '加载商品信息失败');
|
||
window.location.href = '/merchant/products.html';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
alert('加载商品信息失败');
|
||
window.location.href = '/merchant/products.html';
|
||
}
|
||
}
|
||
|
||
// 渲染规格列表
|
||
function renderSpecs() {
|
||
const container = document.getElementById('specsList');
|
||
if (specs.length === 0) {
|
||
container.innerHTML = '<p class="text-muted">暂无规格,点击"添加规格"按钮添加</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="table table-sm table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th>规格名称</th>
|
||
<th>价格调整</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${specs.map((spec, index) => `
|
||
<tr>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm"
|
||
value="${spec.specName || ''}"
|
||
onchange="updateSpec(${index}, 'specName', this.value)"
|
||
placeholder="例如:大杯、中杯、小杯">
|
||
</td>
|
||
<td>
|
||
<input type="number" class="form-control form-control-sm"
|
||
value="${spec.priceAdjust || 0}"
|
||
step="0.01"
|
||
onchange="updateSpec(${index}, 'priceAdjust', this.value)"
|
||
placeholder="价格调整">
|
||
</td>
|
||
<td>
|
||
<button type="button" class="btn btn-sm btn-danger" onclick="removeSpec(${index})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
// 渲染配料列表
|
||
function renderToppings() {
|
||
const container = document.getElementById('toppingsList');
|
||
if (toppings.length === 0) {
|
||
container.innerHTML = '<p class="text-muted">暂无配料,点击"添加配料"按钮添加</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="table table-sm table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th>配料名称</th>
|
||
<th>价格</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${toppings.map((topping, index) => `
|
||
<tr>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm"
|
||
value="${topping.toppingName || ''}"
|
||
onchange="updateTopping(${index}, 'toppingName', this.value)"
|
||
placeholder="例如:珍珠、椰果、布丁">
|
||
</td>
|
||
<td>
|
||
<input type="number" class="form-control form-control-sm"
|
||
value="${topping.price || 0}"
|
||
step="0.01"
|
||
onchange="updateTopping(${index}, 'price', this.value)"
|
||
placeholder="价格">
|
||
</td>
|
||
<td>
|
||
<button type="button" class="btn btn-sm btn-danger" onclick="removeTopping(${index})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
// 添加规格
|
||
function addSpec() {
|
||
specs.push({ specName: '', priceAdjust: 0 });
|
||
renderSpecs();
|
||
}
|
||
|
||
// 更新规格
|
||
function updateSpec(index, field, value) {
|
||
if (specs[index]) {
|
||
specs[index][field] = field === 'priceAdjust' ? parseFloat(value) || 0 : value;
|
||
}
|
||
}
|
||
|
||
// 删除规格
|
||
function removeSpec(index) {
|
||
if (confirm('确定要删除这个规格吗?')) {
|
||
specs.splice(index, 1);
|
||
renderSpecs();
|
||
}
|
||
}
|
||
|
||
// 添加配料
|
||
function addTopping() {
|
||
toppings.push({ toppingName: '', price: 0 });
|
||
renderToppings();
|
||
}
|
||
|
||
// 更新配料
|
||
function updateTopping(index, field, value) {
|
||
if (toppings[index]) {
|
||
toppings[index][field] = field === 'price' ? parseFloat(value) || 0 : value;
|
||
}
|
||
}
|
||
|
||
// 删除配料
|
||
function removeTopping(index) {
|
||
if (confirm('确定要删除这个配料吗?')) {
|
||
toppings.splice(index, 1);
|
||
renderToppings();
|
||
}
|
||
}
|
||
|
||
// 渲染定制选项
|
||
function renderCustoms() {
|
||
renderCustomType('sweetness');
|
||
renderCustomType('ice');
|
||
}
|
||
|
||
// 渲染特定类型的定制选项
|
||
function renderCustomType(type) {
|
||
const container = document.getElementById(type + 'List');
|
||
const options = customs[type] || [];
|
||
|
||
if (options.length === 0) {
|
||
container.innerHTML = '<p class="text-muted">暂无' + (type === 'sweetness' ? '甜度' : '冰度') + '选项,点击"添加' + (type === 'sweetness' ? '甜度' : '冰度') + '"按钮添加</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="table table-sm table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th>选项值</th>
|
||
<th>价格调整</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${options.map((custom, index) => `
|
||
<tr>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm"
|
||
value="${custom.optionValue || ''}"
|
||
onchange="updateCustom('${type}', ${index}, 'optionValue', this.value)"
|
||
placeholder="例如:${type === 'sweetness' ? '无糖、少糖、正常、多糖' : '去冰、少冰、正常、多冰'}">
|
||
</td>
|
||
<td>
|
||
<input type="number" class="form-control form-control-sm"
|
||
value="${custom.priceAdjust || 0}"
|
||
step="0.01"
|
||
onchange="updateCustom('${type}', ${index}, 'priceAdjust', this.value)"
|
||
placeholder="价格调整">
|
||
</td>
|
||
<td>
|
||
<button type="button" class="btn btn-sm btn-danger" onclick="removeCustom('${type}', ${index})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
// 添加定制选项
|
||
function addCustom(type) {
|
||
if (!customs[type]) {
|
||
customs[type] = [];
|
||
}
|
||
customs[type].push({
|
||
optionType: type,
|
||
optionValue: '',
|
||
priceAdjust: 0
|
||
});
|
||
renderCustomType(type);
|
||
}
|
||
|
||
// 更新定制选项
|
||
function updateCustom(type, index, field, value) {
|
||
if (customs[type] && customs[type][index]) {
|
||
customs[type][index][field] = field === 'priceAdjust' ? parseFloat(value) || 0 : value;
|
||
}
|
||
}
|
||
|
||
// 删除定制选项
|
||
function removeCustom(type, index) {
|
||
if (confirm('确定要删除这个选项吗?')) {
|
||
customs[type].splice(index, 1);
|
||
renderCustomType(type);
|
||
}
|
||
}
|
||
|
||
// 提交表单
|
||
document.getElementById('productForm').addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
// 表单验证
|
||
if (!UIEnhancements.validateForm('productForm')) {
|
||
UIEnhancements.showWarning('请填写所有必填项');
|
||
return;
|
||
}
|
||
|
||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||
const formData = new FormData();
|
||
formData.append('name', document.getElementById('name').value);
|
||
formData.append('category', document.getElementById('category').value);
|
||
formData.append('description', document.getElementById('description').value);
|
||
formData.append('basePrice', document.getElementById('basePrice').value);
|
||
|
||
const imageFile = document.getElementById('image').files[0];
|
||
if (imageFile) {
|
||
formData.append('image', imageFile);
|
||
}
|
||
|
||
const productIdValue = document.getElementById('productId').value;
|
||
const url = productIdValue ? `/merchant/products/${productIdValue}` : '/merchant/products';
|
||
const method = productIdValue ? 'PUT' : 'POST';
|
||
|
||
try {
|
||
UIEnhancements.setButtonLoading(submitBtn, true);
|
||
|
||
// 先保存商品基本信息
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
credentials: 'include',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.code === 200) {
|
||
const savedProductId = data.data.id || productIdValue;
|
||
|
||
// 保存规格、配料和定制选项(如果有商品ID)
|
||
if (savedProductId) {
|
||
await saveSpecsAndToppings(savedProductId);
|
||
await saveCustoms(savedProductId);
|
||
}
|
||
|
||
UIEnhancements.showSuccess('商品保存成功!');
|
||
setTimeout(() => {
|
||
window.location.href = '/merchant/products.html';
|
||
}, 1000);
|
||
} else {
|
||
if (data.message && data.message.includes('登录')) {
|
||
UIEnhancements.showWarning('请先登录');
|
||
setTimeout(() => {
|
||
window.location.href = '/merchant/login.html';
|
||
}, 1500);
|
||
} else {
|
||
UIEnhancements.showError(data.message || '保存失败');
|
||
}
|
||
UIEnhancements.setButtonLoading(submitBtn, false);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
UIEnhancements.showError('保存失败,请检查网络连接');
|
||
UIEnhancements.setButtonLoading(submitBtn, false);
|
||
}
|
||
});
|
||
|
||
// 保存规格和配料
|
||
async function saveSpecsAndToppings(productId) {
|
||
// 获取现有的规格和配料
|
||
const existingResponse = await fetch(`/merchant/products/${productId}`, {
|
||
credentials: 'include'
|
||
});
|
||
const existingData = await existingResponse.json();
|
||
const existingSpecs = existingData.code === 200 ? (existingData.data.specs || []) : [];
|
||
const existingToppings = existingData.code === 200 ? (existingData.data.toppings || []) : [];
|
||
|
||
// 删除所有现有规格和配料
|
||
for (const spec of existingSpecs) {
|
||
if (spec.id) {
|
||
await fetch(`/merchant/products/specs/${spec.id}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
}
|
||
}
|
||
for (const topping of existingToppings) {
|
||
if (topping.id) {
|
||
await fetch(`/merchant/products/toppings/${topping.id}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加新的规格
|
||
for (const spec of specs) {
|
||
if (spec.specName && spec.specName.trim()) {
|
||
await fetch(`/merchant/products/${productId}/specs`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
specName: spec.specName,
|
||
priceAdjust: spec.priceAdjust || 0
|
||
})
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加新的配料
|
||
for (const topping of toppings) {
|
||
if (topping.toppingName && topping.toppingName.trim()) {
|
||
await fetch(`/merchant/products/${productId}/toppings`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
toppingName: topping.toppingName,
|
||
price: topping.price || 0
|
||
})
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存定制选项
|
||
async function saveCustoms(productId) {
|
||
// 获取现有的定制选项
|
||
const existingResponse = await fetch(`/merchant/products/${productId}/customs`, {
|
||
credentials: 'include'
|
||
});
|
||
const existingData = await existingResponse.json();
|
||
const existingCustoms = existingData.code === 200 ? (existingData.data || []) : [];
|
||
|
||
// 删除所有现有定制选项
|
||
for (const custom of existingCustoms) {
|
||
if (custom.id) {
|
||
await fetch(`/merchant/products/customs/${custom.id}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加新的甜度选项
|
||
for (const custom of customs.sweetness) {
|
||
if (custom.optionValue && custom.optionValue.trim()) {
|
||
await fetch(`/merchant/products/${productId}/customs`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
optionType: 'sweetness',
|
||
optionValue: custom.optionValue,
|
||
priceAdjust: custom.priceAdjust || 0
|
||
})
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加新的冰度选项
|
||
for (const custom of customs.ice) {
|
||
if (custom.optionValue && custom.optionValue.trim()) {
|
||
await fetch(`/merchant/products/${productId}/customs`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
optionType: 'ice',
|
||
optionValue: custom.optionValue,
|
||
priceAdjust: custom.priceAdjust || 0
|
||
})
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|