467 lines
20 KiB
HTML
467 lines
20 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">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||
<style>
|
||
/* 侧边栏样式 */
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 260px;
|
||
height: 100vh;
|
||
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
|
||
color: #fff;
|
||
z-index: 1050;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: transform 0.3s ease;
|
||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 1.5rem;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.sidebar-brand {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem 0;
|
||
}
|
||
|
||
.sidebar-nav .nav-link {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
padding: 0.75rem 1.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
transition: all 0.2s;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.sidebar-nav .nav-link:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
color: #fff;
|
||
}
|
||
|
||
.sidebar-nav .nav-link.active {
|
||
background-color: rgba(255, 255, 255, 0.15);
|
||
color: #fff;
|
||
border-left: 3px solid #3498db;
|
||
}
|
||
|
||
.sidebar-nav .nav-link i {
|
||
width: 20px;
|
||
margin-right: 0.75rem;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 260px;
|
||
padding: 2rem;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.sidebar.show {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
</style>
|
||
</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" href="/merchant/products.html">商品管理</a>
|
||
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
|
||
<a class="nav-link active" 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">
|
||
<div class="d-flex justify-content-between mb-3">
|
||
<h2>库存管理</h2>
|
||
<button class="btn btn-primary" onclick="showAddInventoryModal()">新增库存</button>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<button class="btn btn-outline-warning" onclick="loadLowStockAlerts()">低库存预警</button>
|
||
</div>
|
||
|
||
<div id="inventoryList"></div>
|
||
</div>
|
||
|
||
<!-- 添加/编辑库存模态框 -->
|
||
<div class="modal fade" id="inventoryModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="inventoryModalTitle">新增库存</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="inventoryForm">
|
||
<input type="hidden" id="inventoryId">
|
||
<div class="mb-3">
|
||
<label for="materialName" class="form-label">原料名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="materialName" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="quantity" class="form-label">数量 <span class="text-danger">*</span></label>
|
||
<input type="number" class="form-control" id="quantity" step="0.01" min="0" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="unit" class="form-label">单位</label>
|
||
<input type="text" class="form-control" id="unit" placeholder="例如:kg、L、个">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="minThreshold" class="form-label">最低库存阈值</label>
|
||
<input type="number" class="form-control" id="minThreshold" step="0.01" min="0" value="0">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveInventory()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 入库/出库模态框 -->
|
||
<div class="modal fade" id="stockModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="stockModalTitle">入库</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="stockForm">
|
||
<input type="hidden" id="stockInventoryId">
|
||
<div class="mb-3">
|
||
<label for="stockQuantity" class="form-label">数量 <span class="text-danger">*</span></label>
|
||
<input type="number" class="form-control" id="stockQuantity" step="0.01" min="0" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="operator" class="form-label">操作人</label>
|
||
<input type="text" class="form-control" id="operator">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveStock()">确认</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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;
|
||
}
|
||
}
|
||
|
||
let stockOperationType = 'in'; // 'in' 或 'out'
|
||
|
||
// 页面加载
|
||
checkLogin().then(isLoggedIn => {
|
||
if (isLoggedIn) {
|
||
loadInventory();
|
||
}
|
||
});
|
||
|
||
// 加载库存列表
|
||
async function loadInventory() {
|
||
try {
|
||
const response = await fetch('/merchant/inventory', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
const container = document.getElementById('inventoryList');
|
||
|
||
if (data.code === 200 && data.data) {
|
||
const inventory = data.data || [];
|
||
|
||
if (inventory.length === 0) {
|
||
container.innerHTML = '<div class="alert alert-info">暂无库存记录,请点击"新增库存"按钮添加</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>原料名称</th>
|
||
<th>数量</th>
|
||
<th>单位</th>
|
||
<th>最低阈值</th>
|
||
<th>库存状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${inventory.map(item => {
|
||
const isLowStock = parseFloat(item.quantity || 0) <= parseFloat(item.minThreshold || 0);
|
||
return `
|
||
<tr class="${isLowStock ? 'table-warning' : ''}">
|
||
<td>${item.materialName || ''}</td>
|
||
<td>${item.quantity || '0'}</td>
|
||
<td>${item.unit || '-'}</td>
|
||
<td>${item.minThreshold || '0'}</td>
|
||
<td>
|
||
${isLowStock ?
|
||
'<span class="badge bg-warning">库存不足</span>' :
|
||
'<span class="badge bg-success">正常</span>'
|
||
}
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-success" onclick="showStockModal(${item.id}, 'in')">入库</button>
|
||
<button class="btn btn-sm btn-warning" onclick="showStockModal(${item.id}, 'out')">出库</button>
|
||
<button class="btn btn-sm btn-primary" onclick="editInventory(${item.id})">编辑</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteInventory(${item.id})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
} else {
|
||
container.innerHTML = '<div class="alert alert-warning">加载库存列表失败:' + (data.message || '未知错误') + '</div>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
document.getElementById('inventoryList').innerHTML = '<div class="alert alert-danger">加载库存列表失败,请刷新页面重试</div>';
|
||
}
|
||
}
|
||
|
||
// 加载低库存预警
|
||
async function loadLowStockAlerts() {
|
||
try {
|
||
const response = await fetch('/merchant/inventory/alerts', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.code === 200 && data.data) {
|
||
const alerts = data.data || [];
|
||
if (alerts.length === 0) {
|
||
alert('暂无低库存预警');
|
||
} else {
|
||
alert(`有 ${alerts.length} 个原料库存不足:\n${alerts.map(a => `${a.materialName} (当前: ${a.quantity}, 阈值: ${a.minThreshold})`).join('\n')}`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
alert('加载低库存预警失败');
|
||
}
|
||
}
|
||
|
||
// 显示添加库存模态框
|
||
function showAddInventoryModal() {
|
||
document.getElementById('inventoryModalTitle').textContent = '新增库存';
|
||
document.getElementById('inventoryId').value = '';
|
||
document.getElementById('inventoryForm').reset();
|
||
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// 编辑库存
|
||
async function editInventory(inventoryId) {
|
||
try {
|
||
const response = await fetch('/merchant/inventory', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.code === 200 && data.data) {
|
||
const inventory = data.data.find(i => i.id === inventoryId);
|
||
|
||
if (inventory) {
|
||
document.getElementById('inventoryModalTitle').textContent = '编辑库存';
|
||
document.getElementById('inventoryId').value = inventory.id;
|
||
document.getElementById('materialName').value = inventory.materialName || '';
|
||
document.getElementById('quantity').value = inventory.quantity || '';
|
||
document.getElementById('unit').value = inventory.unit || '';
|
||
document.getElementById('minThreshold').value = inventory.minThreshold || '0';
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
|
||
modal.show();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
}
|
||
}
|
||
|
||
// 保存库存
|
||
async function saveInventory() {
|
||
const inventoryId = document.getElementById('inventoryId').value;
|
||
const inventoryData = {
|
||
materialName: document.getElementById('materialName').value,
|
||
quantity: parseFloat(document.getElementById('quantity').value) || 0,
|
||
unit: document.getElementById('unit').value,
|
||
minThreshold: parseFloat(document.getElementById('minThreshold').value) || 0
|
||
};
|
||
|
||
if (!inventoryData.materialName) {
|
||
UIEnhancements.showWarning('请填写材料名称');
|
||
return;
|
||
}
|
||
|
||
const saveBtn = document.querySelector('#inventoryModal .btn-primary');
|
||
|
||
try {
|
||
const url = inventoryId ? `/merchant/inventory/${inventoryId}` : '/merchant/inventory';
|
||
const method = inventoryId ? 'PUT' : 'POST';
|
||
|
||
const { data } = await UIEnhancements.enhancedFetch(url, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify(inventoryData),
|
||
button: saveBtn
|
||
});
|
||
|
||
if (data.code === 200) {
|
||
UIEnhancements.showSuccess(inventoryId ? '库存信息已更新' : '库存已添加');
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('inventoryModal'));
|
||
modal.hide();
|
||
loadInventory();
|
||
} else {
|
||
UIEnhancements.showError(data.message || '保存失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
UIEnhancements.showError('保存失败,请稍后重试');
|
||
}
|
||
}
|
||
|
||
// 显示入库/出库模态框
|
||
function showStockModal(inventoryId, type) {
|
||
stockOperationType = type;
|
||
document.getElementById('stockModalTitle').textContent = type === 'in' ? '入库' : '出库';
|
||
document.getElementById('stockInventoryId').value = inventoryId;
|
||
document.getElementById('stockForm').reset();
|
||
const modal = new bootstrap.Modal(document.getElementById('stockModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// 保存入库/出库
|
||
async function saveStock() {
|
||
const inventoryId = document.getElementById('stockInventoryId').value;
|
||
const quantity = parseFloat(document.getElementById('stockQuantity').value);
|
||
const operator = document.getElementById('operator').value;
|
||
|
||
if (!quantity || quantity <= 0) {
|
||
UIEnhancements.showWarning('请输入有效的数量');
|
||
return;
|
||
}
|
||
|
||
const saveBtn = document.querySelector('#stockModal .btn-primary');
|
||
|
||
try {
|
||
const url = `/merchant/inventory/${inventoryId}/${stockOperationType}`;
|
||
const { data } = await UIEnhancements.enhancedFetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
credentials: 'include',
|
||
body: `quantity=${quantity}&operator=${encodeURIComponent(operator || '')}`,
|
||
button: saveBtn
|
||
});
|
||
|
||
if (data.code === 200) {
|
||
UIEnhancements.showSuccess(stockOperationType === 'in' ? '入库成功' : '出库成功');
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('stockModal'));
|
||
modal.hide();
|
||
loadInventory();
|
||
} else {
|
||
UIEnhancements.showError(data.message || '操作失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
UIEnhancements.showError('操作失败,请稍后重试');
|
||
}
|
||
}
|
||
|
||
// 删除库存
|
||
async function deleteInventory(inventoryId) {
|
||
const confirmed = await UIEnhancements.confirmAction('确定要删除这个库存记录吗?', '确认删除');
|
||
if (!confirmed) return;
|
||
|
||
try {
|
||
const { data } = await UIEnhancements.enhancedFetch(`/merchant/inventory/${inventoryId}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (data.code === 200) {
|
||
UIEnhancements.showSuccess('库存记录已删除');
|
||
loadInventory();
|
||
} else {
|
||
UIEnhancements.showError(data.message || '删除失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
UIEnhancements.showError('删除失败,请稍后重试');
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|