Files
2026-01-11 18:14:42 +08:00

652 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>