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

467 lines
20 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">
<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>