783 lines
31 KiB
JavaScript
Executable File
783 lines
31 KiB
JavaScript
Executable File
// 期初库存管理
|
||
(() => {
|
||
let stockList = [];
|
||
let currentPage = 1;
|
||
const pageSize = 20;
|
||
let editingId = null;
|
||
let currentFactory = '';
|
||
|
||
Router.register('/plan-mgmt/initial-stock', async () => {
|
||
const html = `
|
||
<style>
|
||
#initial-stock-page {
|
||
padding: 20px;
|
||
background: var(--bg);
|
||
}
|
||
|
||
#initial-stock-page .page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
#initial-stock-page h1 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
}
|
||
|
||
#initial-stock-page .content-area {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
#initial-stock-page .filter-section {
|
||
padding: 20px;
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
#initial-stock-page .table-wrapper {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
#initial-stock-page table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 0;
|
||
}
|
||
|
||
#initial-stock-page thead {
|
||
background: var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
#initial-stock-page th {
|
||
padding: 8px 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
border-bottom: 2px solid var(--border);
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#initial-stock-page td {
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
}
|
||
|
||
#initial-stock-page tbody tr:hover {
|
||
background: var(--hover);
|
||
}
|
||
|
||
#initial-stock-page .text-center {
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
|
||
<div id="initial-stock-page">
|
||
<div class="page-header">
|
||
<h1 style="margin: 0; font-size: 24px;">期初库存管理</h1>
|
||
<div class="page-actions" style="margin-top: 16px;">
|
||
<button id="add-stock-btn" class="btn btn-primary" style="margin-right: 10px;">新增库存</button>
|
||
<button id="import-stock-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
|
||
<button id="download-template-btn" class="btn btn-secondary" style="margin-right: 10px;">下载模板</button>
|
||
<button id="restore-backup-btn" class="btn btn-secondary" style="margin-right: 10px;">📦 数据恢复</button>
|
||
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-area">
|
||
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center; justify-content: flex-start;">
|
||
<select id="factory-filter" class="input" style="width: 200px;" onchange="InitialStock.filterByFactory()">
|
||
<option value="">全部工厂</option>
|
||
</select>
|
||
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码、名称或供应商" style="flex: 1; max-width: 400px;" />
|
||
<button class="btn btn-secondary" onclick="InitialStock.search()">搜索</button>
|
||
<button class="btn btn-secondary" onclick="InitialStock.resetSearch()">重置</button>
|
||
</div>
|
||
|
||
<div class="table-wrapper">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="InitialStock.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
|
||
<th>物料编码</th>
|
||
<th>物料名称</th>
|
||
<th>库存数量</th>
|
||
<th>单位</th>
|
||
<th>最小包装</th>
|
||
<th>供应商</th>
|
||
<th>工厂</th>
|
||
<th>备注</th>
|
||
<th>更新时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="stock-list">
|
||
<tr><td colspan="10" class="text-center">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="pagination" id="pagination" style="padding: 20px; display: flex; justify-content: center; gap: 8px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Excel导入弹窗 -->
|
||
<div id="import-excel-modal" class="modal" style="display:none;">
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h2>导入期初库存清单</h2>
|
||
<button class="modal-close" onclick="InitialStock.closeImportModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="field">
|
||
<label>默认工厂(可选)</label>
|
||
<div style="display: flex; gap: 5px;">
|
||
<input type="text" id="default-factory" class="input" placeholder="不填写则使用Excel中的工厂值" style="flex: 1;" />
|
||
<select id="default-factory-select" onchange="document.getElementById('default-factory').value = this.value;" style="width: 120px;">
|
||
<option value="">快速选择</option>
|
||
</select>
|
||
</div>
|
||
<div style="margin-top: 4px; font-size: 12px; color: var(--text-3);">
|
||
如果Excel中工厂列为空,将使用此默认值;如果都未填写,则标记为"未指定"
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>选择Excel文件 <span style="color: var(--danger);">*</span></label>
|
||
<input type="file" id="excel-file" class="input" accept=".xlsx,.xls,.csv" />
|
||
<div style="margin-top: 8px; font-size: 12px; color: var(--text-3);">
|
||
支持 .xlsx、.xls 和 .csv 格式,文件大小不超过 10MB
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
|
||
<div style="font-size: 14px; color: var(--text-2); margin-bottom: 12px;">
|
||
<strong>Excel文件格式要求:</strong>
|
||
</div>
|
||
<table style="width: 100%; font-size: 13px; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: var(--surface-2);">
|
||
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">列名</th>
|
||
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">说明</th>
|
||
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">必填</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">物料编码</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">物料编码</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">物料名称</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">物料名称</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">库存数量</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">当前库存数量</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">单位</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">单位(如:pcs)</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">最小包装</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">最小包装数量</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">供应商</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">供应商名称</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">工厂</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">工厂名称(可输入自定义名称,如:友辉、铨宝)</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✗</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">备注</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border;">备注信息</td>
|
||
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✗</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="InitialStock.closeImportModal()">取消</button>
|
||
<button class="btn btn-primary" onclick="InitialStock.importExcel()">导入</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑库存弹窗 -->
|
||
<div id="stock-modal" class="modal" style="display:none;">
|
||
<div class="modal-content" style="max-width: 500px;">
|
||
<div class="modal-header">
|
||
<h2 id="modal-title">新增期初库存</h2>
|
||
<button class="modal-close" onclick="InitialStock.closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="stock-form">
|
||
<div class="field">
|
||
<label>物料编码 <span style="color: var(--danger);">*</span></label>
|
||
<input type="text" id="material-code" class="input" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>物料名称 <span style="color: var(--danger);">*</span></label>
|
||
<input type="text" id="material-name" class="input" required />
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||
<div class="field">
|
||
<label>库存数量 <span style="color: var(--danger);">*</span></label>
|
||
<input type="number" id="stock-qty" class="input" min="0" value="0" required />
|
||
</div>
|
||
<div class="field">
|
||
<label>单位</label>
|
||
<input type="text" id="unit" class="input" value="pcs" />
|
||
</div>
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||
<div class="field">
|
||
<label>最小包装</label>
|
||
<input type="number" id="min-package" class="input" min="1" value="1" />
|
||
</div>
|
||
<div class="field">
|
||
<label>供应商</label>
|
||
<input type="text" id="supplier" class="input" />
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>工厂</label>
|
||
<div style="display: flex; gap: 5px;">
|
||
<input type="text" id="factory" class="input" placeholder="请输入工厂名称" style="flex: 1;" />
|
||
<select id="factory-select" onchange="document.getElementById('factory').value = this.value;" style="width: 120px;">
|
||
<option value="">快速选择</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>备注</label>
|
||
<textarea id="remark" class="input" rows="2"></textarea>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="InitialStock.closeModal()">取消</button>
|
||
<button class="btn btn-primary" onclick="InitialStock.save()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 数据恢复弹窗 -->
|
||
<div id="restore-modal" class="modal" style="display:none;">
|
||
<div class="modal-content" style="max-width: 800px;">
|
||
<div class="modal-header">
|
||
<h2>📦 数据恢复</h2>
|
||
<button class="modal-close" onclick="InitialStock.closeRestoreModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div style="margin-bottom: 16px; padding: 12px; background: var(--warning-bg, #fff3cd); border-left: 4px solid var(--warning, #ffc107); border-radius: 4px;">
|
||
<strong>⚠️ 重要提示:</strong>
|
||
<ul style="margin: 8px 0 0 20px; padding: 0;">
|
||
<li>恢复数据将<strong>完全替换</strong>当前的期初库存数据</li>
|
||
<li>恢复前会自动备份当前数据,可以再次恢复</li>
|
||
<li>请仔细确认要恢复的备份版本</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 12px;">
|
||
<strong>可用备份列表:</strong>
|
||
</div>
|
||
|
||
<div id="backup-list" style="max-height: 400px; overflow-y: auto;">
|
||
<div style="text-align: center; padding: 40px; color: var(--text-3);">加载中...</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="InitialStock.closeRestoreModal()">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
setTimeout(() => {
|
||
document.getElementById('add-stock-btn')?.addEventListener('click', () => openModal());
|
||
document.getElementById('import-stock-btn')?.addEventListener('click', () => showImportDialog());
|
||
document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
|
||
document.getElementById('restore-backup-btn')?.addEventListener('click', () => showRestoreDialog());
|
||
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
|
||
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') InitialStock.search();
|
||
});
|
||
loadList();
|
||
loadFactories();
|
||
}, 100);
|
||
|
||
return html;
|
||
});
|
||
|
||
async function showImportDialog() {
|
||
document.getElementById('excel-file').value = '';
|
||
document.getElementById('default-factory').value = '';
|
||
document.getElementById('import-excel-modal').style.display = 'flex';
|
||
|
||
// 动态加载工厂列表
|
||
await loadFactoriesForSelect('default-factory-select');
|
||
}
|
||
|
||
function closeImportModal() {
|
||
document.getElementById('import-excel-modal').style.display = 'none';
|
||
}
|
||
|
||
async function importExcel() {
|
||
const fileInput = document.getElementById('excel-file');
|
||
const file = fileInput.files[0];
|
||
const defaultFactory = document.getElementById('default-factory').value.trim();
|
||
|
||
if (!file) {
|
||
alert('请选择要导入的文件');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
if (defaultFactory) {
|
||
formData.append('default_factory', defaultFactory);
|
||
}
|
||
|
||
try {
|
||
const overlay = document.getElementById('overlay');
|
||
overlay.classList.remove('hidden');
|
||
|
||
const res = await fetch('/api/initial-stock/import', {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'include'
|
||
});
|
||
|
||
overlay.classList.add('hidden');
|
||
|
||
const data = await res.json();
|
||
if (data.ok) {
|
||
alert(data.message || '导入成功');
|
||
closeImportModal();
|
||
loadList();
|
||
loadFactories(); // 重新加载工厂列表
|
||
} else {
|
||
alert(data.error || '导入失败');
|
||
}
|
||
} catch (err) {
|
||
document.getElementById('overlay').classList.add('hidden');
|
||
alert('导入失败: ' + err.message);
|
||
}
|
||
}
|
||
|
||
async function loadFactories() {
|
||
try {
|
||
const res = await API.get('/api/factories');
|
||
const factories = res.factories || [];
|
||
const select = document.getElementById('factory-filter');
|
||
if (select) {
|
||
// 保留"全部工厂"选项,清空其他选项
|
||
select.innerHTML = '<option value="">全部工厂</option>';
|
||
factories.forEach(factory => {
|
||
const option = document.createElement('option');
|
||
option.value = factory;
|
||
option.textContent = factory;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('加载工厂列表失败:', e);
|
||
}
|
||
}
|
||
|
||
async function loadFactoriesForSelect(selectId) {
|
||
try {
|
||
const res = await API.get('/api/factories');
|
||
const factories = res.factories || [];
|
||
const select = document.getElementById(selectId);
|
||
if (select) {
|
||
const currentValue = select.value;
|
||
const firstOption = select.options[0] ? select.options[0].outerHTML : '<option value="">快速选择</option>';
|
||
select.innerHTML = firstOption;
|
||
factories.forEach(factory => {
|
||
const option = document.createElement('option');
|
||
option.value = factory;
|
||
option.textContent = factory;
|
||
select.appendChild(option);
|
||
});
|
||
if (currentValue) {
|
||
select.value = currentValue;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('加载工厂列表失败:', e);
|
||
}
|
||
}
|
||
|
||
function downloadTemplate() {
|
||
const headers = ['物料编码', '物料名称', '库存数量', '单位', '最小包装', '供应商', '工厂', '备注'];
|
||
const example1 = ['PCB-001', '主控PCB板', '50', 'pcs', '10', '深圳电子', '友辉', ''];
|
||
const example2 = ['PCB-002', '电源板', '100', 'pcs', '20', '东莞电子', '铨宝', ''];
|
||
const example3 = ['PCB-003', '显示板', '75', 'pcs', '15', '苏州电子', '新工厂', ''];
|
||
|
||
const csvContent = [headers.join(','), example1.join(','), example2.join(','), example3.join(',')].join('\n');
|
||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = '期初库存导入模板.csv';
|
||
link.click();
|
||
}
|
||
|
||
async function loadList() {
|
||
try {
|
||
let url = '/api/initial-stock';
|
||
if (currentFactory) {
|
||
url += `?factory=${encodeURIComponent(currentFactory)}`;
|
||
}
|
||
const res = await API.get(url);
|
||
stockList = res.list || [];
|
||
renderList();
|
||
} catch (e) {
|
||
console.error('加载库存列表失败:', e);
|
||
document.getElementById('stock-list').innerHTML = '<tr><td colspan="11" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
|
||
}
|
||
}
|
||
|
||
function renderList() {
|
||
const tbody = document.getElementById('stock-list');
|
||
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
|
||
|
||
let filtered = stockList;
|
||
if (keyword) {
|
||
filtered = stockList.filter(item =>
|
||
(item.material_code || '').toLowerCase().includes(keyword) ||
|
||
(item.material_name || '').toLowerCase().includes(keyword) ||
|
||
(item.supplier || '').toLowerCase().includes(keyword)
|
||
);
|
||
}
|
||
|
||
const totalPages = Math.ceil(filtered.length / pageSize);
|
||
const start = (currentPage - 1) * pageSize;
|
||
const pageData = filtered.slice(start, start + pageSize);
|
||
|
||
if (pageData.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="11" class="text-center">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = pageData.map(item => `
|
||
<tr>
|
||
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" onchange="InitialStock.updateSelectAll()" /><span class="checkmark"></span></label></td>
|
||
<td>${escapeHtml(item.material_code || '')}</td>
|
||
<td>${escapeHtml(item.material_name || '')}</td>
|
||
<td style="font-weight: 600; color: var(--primary);">${item.stock_qty || 0}</td>
|
||
<td>${escapeHtml(item.unit || 'pcs')}</td>
|
||
<td>${item.min_package || 1}</td>
|
||
<td>${escapeHtml(item.supplier || '-')}</td>
|
||
<td>${escapeHtml(item.factory || '-')}</td>
|
||
<td>${escapeHtml(item.remark || '-')}</td>
|
||
<td>${formatTime(item.updated_at)}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-secondary" onclick="InitialStock.edit(${item.id})">编辑</button>
|
||
<button class="btn btn-sm btn-danger" onclick="InitialStock.delete(${item.id})">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
renderPagination(totalPages);
|
||
}
|
||
|
||
function renderPagination(totalPages) {
|
||
const container = document.getElementById('pagination');
|
||
if (totalPages <= 1) {
|
||
container.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="InitialStock.goPage(${currentPage - 1})">上一页</button>`;
|
||
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
|
||
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="InitialStock.goPage(${currentPage + 1})">下一页</button>`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
async function openModal(item = null) {
|
||
editingId = item?.id || null;
|
||
document.getElementById('modal-title').textContent = item ? '编辑期初库存' : '新增期初库存';
|
||
document.getElementById('material-code').value = item?.material_code || '';
|
||
document.getElementById('material-name').value = item?.material_name || '';
|
||
document.getElementById('stock-qty').value = item?.stock_qty || 0;
|
||
document.getElementById('unit').value = item?.unit || 'pcs';
|
||
document.getElementById('min-package').value = item?.min_package || 1;
|
||
document.getElementById('supplier').value = item?.supplier || '';
|
||
document.getElementById('factory').value = item?.factory || '';
|
||
document.getElementById('remark').value = item?.remark || '';
|
||
document.getElementById('stock-modal').style.display = 'flex';
|
||
|
||
// 动态加载工厂列表
|
||
await loadFactoriesForSelect('factory-select');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('stock-modal').style.display = 'none';
|
||
editingId = null;
|
||
}
|
||
|
||
async function save() {
|
||
const data = {
|
||
material_code: document.getElementById('material-code').value.trim(),
|
||
material_name: document.getElementById('material-name').value.trim(),
|
||
stock_qty: parseInt(document.getElementById('stock-qty').value) || 0,
|
||
unit: document.getElementById('unit').value.trim() || 'pcs',
|
||
min_package: parseInt(document.getElementById('min-package').value) || 1,
|
||
supplier: document.getElementById('supplier').value.trim(),
|
||
factory: document.getElementById('factory').value.trim(),
|
||
remark: document.getElementById('remark').value.trim()
|
||
};
|
||
|
||
if (!data.material_code || !data.material_name) {
|
||
alert('请填写物料编码和物料名称');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (editingId) {
|
||
await API.put(`/api/initial-stock/${editingId}`, data);
|
||
alert('更新成功');
|
||
} else {
|
||
await API.post('/api/initial-stock', data);
|
||
alert('创建成功');
|
||
}
|
||
closeModal();
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '操作失败');
|
||
}
|
||
}
|
||
|
||
async function edit(id) {
|
||
const item = stockList.find(x => x.id === id);
|
||
if (item) openModal(item);
|
||
}
|
||
|
||
async function deleteStock(id) {
|
||
if (!confirm('确定要删除这条库存记录吗?')) return;
|
||
try {
|
||
await API.delete(`/api/initial-stock/${id}`);
|
||
alert('删除成功');
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '删除失败');
|
||
}
|
||
}
|
||
|
||
async function batchDelete() {
|
||
const checked = document.querySelectorAll('.row-checkbox:checked');
|
||
if (checked.length === 0) {
|
||
alert('请先选择要删除的项');
|
||
return;
|
||
}
|
||
if (!confirm(`确定要删除选中的 ${checked.length} 条库存记录吗?`)) return;
|
||
|
||
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
|
||
try {
|
||
await API.post('/api/initial-stock/batch-delete', { ids });
|
||
alert('批量删除成功');
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '批量删除失败');
|
||
}
|
||
}
|
||
|
||
function toggleSelectAll(checkbox) {
|
||
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
|
||
}
|
||
|
||
function updateSelectAll() {
|
||
const selectAllCheckbox = document.getElementById('select-all');
|
||
const rowCheckboxes = document.querySelectorAll('.row-checkbox');
|
||
selectAllCheckbox.checked = rowCheckboxes.length > 0 && Array.from(rowCheckboxes).every(cb => cb.checked);
|
||
}
|
||
|
||
function search() {
|
||
currentPage = 1;
|
||
renderList();
|
||
}
|
||
|
||
function resetSearch() {
|
||
document.getElementById('search-keyword').value = '';
|
||
document.getElementById('factory-filter').value = '';
|
||
currentFactory = '';
|
||
currentPage = 1;
|
||
loadList();
|
||
}
|
||
|
||
function filterByFactory() {
|
||
const factory = document.getElementById('factory-filter')?.value || '';
|
||
currentFactory = factory;
|
||
currentPage = 1;
|
||
loadList();
|
||
}
|
||
|
||
function goPage(page) {
|
||
currentPage = page;
|
||
renderList();
|
||
}
|
||
|
||
function formatTime(ts) {
|
||
if (!ts) return '-';
|
||
try {
|
||
const d = new Date(ts);
|
||
return d.toLocaleString('zh-CN', { hour12: false });
|
||
} catch {
|
||
return ts;
|
||
}
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
async function showRestoreDialog() {
|
||
document.getElementById('restore-modal').style.display = 'flex';
|
||
await loadBackupList();
|
||
}
|
||
|
||
function closeRestoreModal() {
|
||
document.getElementById('restore-modal').style.display = 'none';
|
||
}
|
||
|
||
async function loadBackupList() {
|
||
const container = document.getElementById('backup-list');
|
||
try {
|
||
const res = await API.get('/api/backups?table_name=initial_stock');
|
||
const backups = res.list || [];
|
||
|
||
if (backups.length === 0) {
|
||
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-3);">暂无备份记录</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = backups.map(backup => `
|
||
<div style="border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 12px; background: white;">
|
||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 600; font-size: 15px; margin-bottom: 8px;">
|
||
${getOperationText(backup.operation)} - ${backup.factory || '全部工厂'}
|
||
</div>
|
||
<div style="font-size: 13px; color: var(--text-2); margin-bottom: 4px;">
|
||
📦 记录数量: <strong>${backup.record_count}</strong> 条
|
||
</div>
|
||
<div style="font-size: 13px; color: var(--text-2); margin-bottom: 4px;">
|
||
👤 操作人: ${backup.created_by || '系统'}
|
||
</div>
|
||
<div style="font-size: 13px; color: var(--text-2); margin-bottom: 4px;">
|
||
🕐 备份时间: ${formatTime(backup.created_at)}
|
||
</div>
|
||
${backup.description ? `<div style="font-size: 13px; color: var(--text-3); margin-top: 8px; padding: 8px; background: var(--surface); border-radius: 4px;">
|
||
💬 ${escapeHtml(backup.description)}
|
||
</div>` : ''}
|
||
</div>
|
||
<div style="display: flex; gap: 8px; margin-left: 16px;">
|
||
<button class="btn btn-sm btn-primary" onclick="InitialStock.restoreBackup(${backup.id})">
|
||
恢复此版本
|
||
</button>
|
||
<button class="btn btn-sm btn-danger" onclick="InitialStock.deleteBackup(${backup.id})">
|
||
删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
container.innerHTML = `<div style="text-align: center; padding: 40px; color: var(--danger);">加载失败: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function getOperationText(operation) {
|
||
const map = {
|
||
'import': '📥 导入前备份',
|
||
'batch_delete': '🗑️ 批量删除前备份',
|
||
'restore': '🔄 恢复前备份',
|
||
'manual': '💾 手动备份'
|
||
};
|
||
return map[operation] || operation;
|
||
}
|
||
|
||
async function restoreBackup(backupId) {
|
||
if (!confirm('确定要恢复此备份吗?\n\n当前数据将被完全替换,但会自动备份当前数据。')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const overlay = document.getElementById('overlay');
|
||
overlay.classList.remove('hidden');
|
||
|
||
const res = await API.post(`/api/backups/${backupId}/restore`, {});
|
||
|
||
overlay.classList.add('hidden');
|
||
|
||
if (res.ok) {
|
||
alert(res.message || '数据恢复成功');
|
||
closeRestoreModal();
|
||
loadList();
|
||
} else {
|
||
alert(res.error || '恢复失败');
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('overlay').classList.add('hidden');
|
||
alert('恢复失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteBackup(backupId) {
|
||
if (!confirm('确定要删除此备份吗?此操作不可恢复。')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await API.delete(`/api/backups/${backupId}`);
|
||
alert('备份已删除');
|
||
await loadBackupList();
|
||
} catch (e) {
|
||
alert('删除失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
window.InitialStock = {
|
||
search,
|
||
resetSearch,
|
||
filterByFactory,
|
||
edit,
|
||
delete: deleteStock,
|
||
closeModal,
|
||
save,
|
||
toggleSelectAll,
|
||
updateSelectAll,
|
||
goPage,
|
||
closeImportModal,
|
||
importExcel,
|
||
closeRestoreModal,
|
||
restoreBackup,
|
||
deleteBackup
|
||
};
|
||
})();
|