智慧水务管理系统 - 精河县供水工程综合管理平台

inventory-frontend.html 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>电商库存管理系统</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. </head>
  9. <body class="bg-gray-50 min-h-screen">
  10. <!-- 导航栏 -->
  11. <nav class="bg-gradient-to-r from-blue-600 to-cyan-600 text-white shadow-lg">
  12. <div class="container mx-auto px-4 py-4">
  13. <div class="flex justify-between items-center">
  14. <div class="flex items-center gap-3">
  15. <span class="text-3xl">📦</span>
  16. <div>
  17. <h1 class="text-2xl font-bold">电商库存管理系统</h1>
  18. <p class="text-blue-200 text-sm">多平台库存同步 • 智能预警</p>
  19. </div>
  20. </div>
  21. <div class="flex gap-4">
  22. <button onclick="showView('products')" class="text-white hover:text-blue-200 transition">📦 商品管理</button>
  23. <button onclick="showView('stock')" class="text-white hover:text-blue-200 transition">📊 库存管理</button>
  24. <button onclick="showView('alerts')" class="text-white hover:text-blue-200 transition">⚠️ 库存预警</button>
  25. <button onclick="showAddModal()" class="bg-white text-blue-600 px-6 py-2 rounded-lg hover:bg-blue-50 font-medium transition shadow">
  26. ➕ 添加商品
  27. </button>
  28. </div>
  29. </div>
  30. </div>
  31. </nav>
  32. <!-- 主内容区 -->
  33. <div class="container mx-auto px-4 py-6">
  34. <!-- 统计卡片 -->
  35. <div class="grid grid-cols-1 md:grid-cols-6 gap-4 mb-6">
  36. <div class="bg-white p-4 rounded-xl shadow-md">
  37. <div class="text-gray-500 text-sm">总商品数</div>
  38. <div class="text-3xl font-bold text-blue-600" id="stat-total">-</div>
  39. </div>
  40. <div class="bg-white p-4 rounded-xl shadow-md">
  41. <div class="text-gray-500 text-sm">在售商品</div>
  42. <div class="text-3xl font-bold text-green-600" id="stat-active">-</div>
  43. </div>
  44. <div class="bg-white p-4 rounded-xl shadow-md">
  45. <div class="text-gray-500 text-sm">库存预警</div>
  46. <div class="text-3xl font-bold text-red-600" id="stat-low">-</div>
  47. </div>
  48. <div class="bg-white p-4 rounded-xl shadow-md">
  49. <div class="text-gray-500 text-sm">库存总值</div>
  50. <div class="text-2xl font-bold text-purple-600" id="stat-value">-</div>
  51. </div>
  52. <div class="bg-white p-4 rounded-xl shadow-md">
  53. <div class="text-gray-500 text-sm">今日入库</div>
  54. <div class="text-3xl font-bold text-green-600" id="stat-in">-</div>
  55. </div>
  56. <div class="bg-white p-4 rounded-xl shadow-md">
  57. <div class="text-gray-500 text-sm">今日出库</div>
  58. <div class="text-3xl font-bold text-orange-600" id="stat-out">-</div>
  59. </div>
  60. </div>
  61. <!-- 商品列表视图 -->
  62. <div id="products-view">
  63. <div class="bg-white rounded-xl shadow-md overflow-hidden">
  64. <div class="px-6 py-4 border-b flex justify-between items-center">
  65. <h2 class="text-lg font-semibold text-gray-800">商品列表</h2>
  66. <div class="flex gap-2">
  67. <input type="text" id="search-product" placeholder="搜索商品..." class="border rounded-lg px-4 py-2">
  68. <button onclick="loadProducts()" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">搜索</button>
  69. </div>
  70. </div>
  71. <table class="w-full">
  72. <thead class="bg-gray-50">
  73. <tr>
  74. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">商品名称</th>
  75. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">SKU</th>
  76. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">分类</th>
  77. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">售价</th>
  78. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">库存</th>
  79. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">状态</th>
  80. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500">操作</th>
  81. </tr>
  82. </thead>
  83. <tbody id="product-list" class="divide-y divide-gray-200">
  84. <tr><td colspan="7" class="px-6 py-12 text-center text-gray-500">⏳ 加载中...</td></tr>
  85. </tbody>
  86. </table>
  87. </div>
  88. </div>
  89. <!-- 库存管理视图 -->
  90. <div id="stock-view" class="hidden">
  91. <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
  92. <!-- 添加入库 -->
  93. <div class="bg-white rounded-xl shadow-md p-6">
  94. <h3 class="text-lg font-bold mb-4">📥 添加入库</h3>
  95. <form id="stock-in-form" class="space-y-4">
  96. <div>
  97. <label class="block text-sm font-medium text-gray-700 mb-2">商品</label>
  98. <select id="stock-in-product" class="w-full border rounded-lg px-4 py-2" required></select>
  99. </div>
  100. <div>
  101. <label class="block text-sm font-medium text-gray-700 mb-2">平台/仓库</label>
  102. <select id="stock-in-platform" class="w-full border rounded-lg px-4 py-2">
  103. <option value="总仓">🏢 总仓</option>
  104. <option value="淘宝">🛒 淘宝</option>
  105. <option value="京东">📦 京东</option>
  106. <option value="拼多多">💰 拼多多</option>
  107. <option value="抖音">🎵 抖音</option>
  108. </select>
  109. </div>
  110. <div>
  111. <label class="block text-sm font-medium text-gray-700 mb-2">数量</label>
  112. <input type="number" id="stock-in-quantity" class="w-full border rounded-lg px-4 py-2" required min="1">
  113. </div>
  114. <div>
  115. <label class="block text-sm font-medium text-gray-700 mb-2">备注</label>
  116. <textarea id="stock-in-note" class="w-full border rounded-lg px-4 py-2" rows="2"></textarea>
  117. </div>
  118. <button type="submit" class="w-full bg-green-600 text-white py-3 rounded-lg hover:bg-green-700 font-medium">✅ 确认入库</button>
  119. </form>
  120. </div>
  121. <!-- 添加出库 -->
  122. <div class="bg-white rounded-xl shadow-md p-6">
  123. <h3 class="text-lg font-bold mb-4">📤 添加出库</h3>
  124. <form id="stock-out-form" class="space-y-4">
  125. <div>
  126. <label class="block text-sm font-medium text-gray-700 mb-2">商品</label>
  127. <select id="stock-out-product" class="w-full border rounded-lg px-4 py-2" required></select>
  128. </div>
  129. <div>
  130. <label class="block text-sm font-medium text-gray-700 mb-2">平台/仓库</label>
  131. <select id="stock-out-platform" class="w-full border rounded-lg px-4 py-2">
  132. <option value="总仓">🏢 总仓</option>
  133. <option value="淘宝">🛒 淘宝</option>
  134. <option value="京东">📦 京东</option>
  135. <option value="拼多多">💰 拼多多</option>
  136. <option value="抖音">🎵 抖音</option>
  137. </select>
  138. </div>
  139. <div>
  140. <label class="block text-sm font-medium text-gray-700 mb-2">数量</label>
  141. <input type="number" id="stock-out-quantity" class="w-full border rounded-lg px-4 py-2" required min="1">
  142. </div>
  143. <div>
  144. <label class="block text-sm font-medium text-gray-700 mb-2">备注</label>
  145. <textarea id="stock-out-note" class="w-full border rounded-lg px-4 py-2" rows="2"></textarea>
  146. </div>
  147. <button type="submit" class="w-full bg-orange-600 text-white py-3 rounded-lg hover:bg-orange-700 font-medium">📤 确认出库</button>
  148. </form>
  149. </div>
  150. </div>
  151. <!-- 库存记录 -->
  152. <div class="bg-white rounded-xl shadow-md mt-6 p-6">
  153. <h3 class="text-lg font-bold mb-4">📋 库存记录</h3>
  154. <div id="stock-records" class="space-y-2">
  155. <p class="text-gray-500 text-center py-8">暂无记录</p>
  156. </div>
  157. </div>
  158. </div>
  159. <!-- 库存预警视图 -->
  160. <div id="alerts-view" class="hidden">
  161. <div class="bg-white rounded-xl shadow-md p-6">
  162. <h3 class="text-lg font-bold mb-4">⚠️ 库存预警</h3>
  163. <div id="alerts-list" class="space-y-4">
  164. <p class="text-gray-500 text-center py-12">✅ 所有商品库存充足</p>
  165. </div>
  166. </div>
  167. </div>
  168. </div>
  169. <!-- 添加商品模态框 -->
  170. <div id="add-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
  171. <div class="bg-white rounded-xl p-8 w-full max-w-lg mx-4 shadow-2xl">
  172. <div class="flex justify-between items-center mb-6">
  173. <h3 class="text-2xl font-bold text-gray-800">➕ 添加商品</h3>
  174. <button onclick="hideAddModal()" class="text-gray-400 hover:text-gray-600 text-2xl">&times;</button>
  175. </div>
  176. <form id="add-form" class="space-y-4">
  177. <div>
  178. <label class="block text-sm font-medium text-gray-700 mb-2">商品名称 *</label>
  179. <input type="text" name="name" required class="w-full border rounded-lg px-4 py-3">
  180. </div>
  181. <div>
  182. <label class="block text-sm font-medium text-gray-700 mb-2">SKU *</label>
  183. <input type="text" name="sku" required class="w-full border rounded-lg px-4 py-3" placeholder="例如:SKU001">
  184. </div>
  185. <div>
  186. <label class="block text-sm font-medium text-gray-700 mb-2">分类</label>
  187. <input type="text" name="category" class="w-full border rounded-lg px-4 py-3">
  188. </div>
  189. <div class="grid grid-cols-2 gap-4">
  190. <div>
  191. <label class="block text-sm font-medium text-gray-700 mb-2">售价</label>
  192. <input type="number" step="0.01" name="price" class="w-full border rounded-lg px-4 py-3">
  193. </div>
  194. <div>
  195. <label class="block text-sm font-medium text-gray-700 mb-2">成本</label>
  196. <input type="number" step="0.01" name="cost" class="w-full border rounded-lg px-4 py-3">
  197. </div>
  198. </div>
  199. <div>
  200. <label class="block text-sm font-medium text-gray-700 mb-2">最低库存预警</label>
  201. <input type="number" name="minStock" value="10" class="w-full border rounded-lg px-4 py-3">
  202. </div>
  203. <div>
  204. <label class="block text-sm font-medium text-gray-700 mb-2">描述</label>
  205. <textarea name="description" rows="3" class="w-full border rounded-lg px-4 py-3"></textarea>
  206. </div>
  207. <div class="flex gap-4 pt-6">
  208. <button type="submit" class="flex-1 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 font-medium">💾 保存</button>
  209. <button type="button" onclick="hideAddModal()" class="flex-1 bg-gray-200 text-gray-700 py-3 rounded-lg hover:bg-gray-300 font-medium">取消</button>
  210. </div>
  211. </form>
  212. </div>
  213. </div>
  214. <script>
  215. const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:3003/api' : '/api';
  216. let products = [];
  217. // 切换视图
  218. function showView(view) {
  219. document.getElementById('products-view').classList.add('hidden');
  220. document.getElementById('stock-view').classList.add('hidden');
  221. document.getElementById('alerts-view').classList.add('hidden');
  222. document.getElementById(view + '-view').classList.remove('hidden');
  223. if (view === 'products') loadProducts();
  224. if (view === 'stock') { loadProducts(); loadStockRecords(); }
  225. if (view === 'alerts') loadAlerts();
  226. loadStats();
  227. }
  228. // 加载统计
  229. async function loadStats() {
  230. try {
  231. const res = await fetch(`${API_BASE}/stats`);
  232. const data = await res.json();
  233. if (data.success) {
  234. document.getElementById('stat-total').textContent = data.data.totalProducts;
  235. document.getElementById('stat-active').textContent = data.data.activeProducts;
  236. document.getElementById('stat-low').textContent = data.data.lowStock;
  237. document.getElementById('stat-value').textContent = '¥' + data.data.totalValue.toFixed(2);
  238. document.getElementById('stat-in').textContent = data.data.todayIn;
  239. document.getElementById('stat-out').textContent = data.data.todayOut;
  240. }
  241. } catch (error) {
  242. console.error('加载统计失败:', error);
  243. }
  244. }
  245. // 加载商品列表
  246. async function loadProducts() {
  247. try {
  248. const keyword = document.getElementById('search-product').value;
  249. const url = `${API_BASE}/products${keyword ? '?keyword=' + encodeURIComponent(keyword) : ''}`;
  250. const res = await fetch(url);
  251. const data = await res.json();
  252. products = data.data || [];
  253. const tbody = document.getElementById('product-list');
  254. if (products.length > 0) {
  255. tbody.innerHTML = products.map(p => `
  256. <tr class="hover:bg-gray-50">
  257. <td class="px-6 py-4 font-medium">${p.name}</td>
  258. <td class="px-6 py-4 text-gray-600">${p.sku}</td>
  259. <td class="px-6 py-4 text-gray-600">${p.category || '-'}</td>
  260. <td class="px-6 py-4 text-green-600 font-medium">¥${p.price}</td>
  261. <td class="px-6 py-4">
  262. <span class="${p.totalStock <= p.minStock ? 'text-red-600 font-bold' : 'text-gray-600'}">${p.totalStock}</span>
  263. </td>
  264. <td class="px-6 py-4">
  265. <span class="px-2 py-1 rounded-full text-xs ${p.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
  266. ${p.status === 'active' ? '在售' : '停售'}
  267. </span>
  268. </td>
  269. <td class="px-6 py-4">
  270. <button onclick="showStockModal(${p.id})" class="text-blue-600 hover:text-blue-800 mr-2">📊 库存</button>
  271. <button onclick="deleteProduct(${p.id})" class="text-red-600 hover:text-red-800">🗑️</button>
  272. </td>
  273. </tr>
  274. `).join('');
  275. } else {
  276. tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-12 text-center text-gray-500">暂无商品</td></tr>';
  277. }
  278. // 填充下拉框
  279. const inSelect = document.getElementById('stock-in-product');
  280. const outSelect = document.getElementById('stock-out-product');
  281. const options = '<option value="">选择商品</option>' + products.map(p =>
  282. `<option value="${p.id}">${p.name} (${p.sku}) - 库存:${p.totalStock}</option>`
  283. ).join('');
  284. inSelect.innerHTML = options;
  285. outSelect.innerHTML = options;
  286. } catch (error) {
  287. console.error('加载商品失败:', error);
  288. }
  289. }
  290. // 加载库存记录
  291. async function loadStockRecords() {
  292. try {
  293. const res = await fetch(`${API_BASE}/stock-records`);
  294. const data = await res.json();
  295. const container = document.getElementById('stock-records');
  296. if (data.data && data.data.length > 0) {
  297. container.innerHTML = data.data.slice(0, 50).map(r => `
  298. <div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
  299. <div>
  300. <span class="${r.type === 'in' ? 'text-green-600' : 'text-orange-600'} font-bold">
  301. ${r.type === 'in' ? '📥 入库' : '📤 出库'}
  302. </span>
  303. <span class="ml-2 text-gray-600">${r.platform}</span>
  304. <span class="ml-2 text-gray-500 text-sm">${new Date(r.created_at).toLocaleString()}</span>
  305. </div>
  306. <div class="text-right">
  307. <span class="${r.type === 'in' ? 'text-green-600' : 'text-orange-600'} font-bold">
  308. ${r.type === 'in' ? '+' : '-'}${r.quantity}
  309. </span>
  310. ${r.note ? `<div class="text-xs text-gray-500">${r.note}</div>` : ''}
  311. </div>
  312. </div>
  313. `).join('');
  314. } else {
  315. container.innerHTML = '<p class="text-gray-500 text-center py-8">暂无库存记录</p>';
  316. }
  317. } catch (error) {
  318. console.error('加载库存记录失败:', error);
  319. }
  320. }
  321. // 加载预警
  322. async function loadAlerts() {
  323. try {
  324. const res = await fetch(`${API_BASE}/alerts`);
  325. const data = await res.json();
  326. const container = document.getElementById('alerts-list');
  327. if (data.data && data.data.length > 0) {
  328. container.innerHTML = data.data.map(a => `
  329. <div class="p-4 rounded-lg ${a.level === 'critical' ? 'bg-red-50 border-l-4 border-red-500' : 'bg-yellow-50 border-l-4 border-yellow-500'}">
  330. <div class="flex justify-between items-center">
  331. <div>
  332. <div class="font-bold ${a.level === 'critical' ? 'text-red-800' : 'text-yellow-800'}">
  333. ${a.level === 'critical' ? '🚨 严重缺货' : '⚠️ 库存不足'}
  334. </div>
  335. <div class="text-gray-700 mt-1">${a.name} (${a.sku})</div>
  336. </div>
  337. <div class="text-right">
  338. <div class="text-2xl font-bold ${a.level === 'critical' ? 'text-red-600' : 'text-yellow-600'}">${a.currentStock}</div>
  339. <div class="text-sm text-gray-500">最低库存:${a.minStock}</div>
  340. </div>
  341. </div>
  342. </div>
  343. `).join('');
  344. } else {
  345. container.innerHTML = '<p class="text-gray-500 text-center py-12">✅ 所有商品库存充足</p>';
  346. }
  347. } catch (error) {
  348. console.error('加载预警失败:', error);
  349. }
  350. }
  351. // 显示库存详情
  352. async function showStockModal(productId) {
  353. const product = products.find(p => p.id === productId);
  354. if (!product) return;
  355. try {
  356. const res = await fetch(`${API_BASE}/platform-stock/${productId}`);
  357. const data = await res.json();
  358. if (data.success) {
  359. const platforms = data.data.byPlatform;
  360. let info = `商品:${product.name}\nSKU: ${product.sku}\n总库存:${data.data.totalStock}\n\n`;
  361. info += '各平台库存:\n';
  362. for (const [platform, stock] of Object.entries(platforms)) {
  363. info += ` ${platform}: ${stock}\n`;
  364. }
  365. alert(info);
  366. }
  367. } catch (error) {
  368. alert('加载库存详情失败');
  369. }
  370. }
  371. // 添加商品
  372. document.getElementById('add-form').addEventListener('submit', async (e) => {
  373. e.preventDefault();
  374. const formData = new FormData(e.target);
  375. const data = Object.fromEntries(formData);
  376. try {
  377. const res = await fetch(`${API_BASE}/products`, {
  378. method: 'POST',
  379. headers: { 'Content-Type': 'application/json' },
  380. body: JSON.stringify(data)
  381. });
  382. const result = await res.json();
  383. if (result.success) {
  384. alert('✅ 商品添加成功!');
  385. hideAddModal();
  386. loadProducts();
  387. loadStats();
  388. }
  389. } catch (error) {
  390. alert('添加失败:' + error.message);
  391. }
  392. });
  393. // 添加入库
  394. document.getElementById('stock-in-form').addEventListener('submit', async (e) => {
  395. e.preventDefault();
  396. const data = {
  397. product_id: document.getElementById('stock-in-product').value,
  398. platform: document.getElementById('stock-in-platform').value,
  399. quantity: parseInt(document.getElementById('stock-in-quantity').value),
  400. note: document.getElementById('stock-in-note').value
  401. };
  402. try {
  403. const res = await fetch(`${API_BASE}/stock-in`, {
  404. method: 'POST',
  405. headers: { 'Content-Type': 'application/json' },
  406. body: JSON.stringify(data)
  407. });
  408. const result = await res.json();
  409. if (result.success) {
  410. alert('✅ 入库成功!');
  411. document.getElementById('stock-in-form').reset();
  412. loadProducts();
  413. loadStats();
  414. loadStockRecords();
  415. }
  416. } catch (error) {
  417. alert('入库失败:' + error.message);
  418. }
  419. });
  420. // 添加出库
  421. document.getElementById('stock-out-form').addEventListener('submit', async (e) => {
  422. e.preventDefault();
  423. const data = {
  424. product_id: document.getElementById('stock-out-product').value,
  425. platform: document.getElementById('stock-out-platform').value,
  426. quantity: parseInt(document.getElementById('stock-out-quantity').value),
  427. note: document.getElementById('stock-out-note').value
  428. };
  429. try {
  430. const res = await fetch(`${API_BASE}/stock-out`, {
  431. method: 'POST',
  432. headers: { 'Content-Type': 'application/json' },
  433. body: JSON.stringify(data)
  434. });
  435. const result = await res.json();
  436. if (result.success) {
  437. alert('✅ 出库成功!');
  438. document.getElementById('stock-out-form').reset();
  439. loadProducts();
  440. loadStats();
  441. loadStockRecords();
  442. }
  443. } catch (error) {
  444. alert('出库失败:' + error.message);
  445. }
  446. });
  447. // 删除商品
  448. async function deleteProduct(id) {
  449. if (!confirm('确定要删除这个商品吗?')) return;
  450. try {
  451. const res = await fetch(`${API_BASE}/products/${id}`, { method: 'DELETE' });
  452. const data = await res.json();
  453. if (data.success) {
  454. alert('✅ 商品已删除');
  455. loadProducts();
  456. loadStats();
  457. }
  458. } catch (error) {
  459. alert('删除失败:' + error.message);
  460. }
  461. }
  462. function showAddModal() {
  463. document.getElementById('add-modal').classList.remove('hidden');
  464. document.getElementById('add-modal').classList.add('flex');
  465. }
  466. function hideAddModal() {
  467. document.getElementById('add-modal').classList.add('hidden');
  468. document.getElementById('add-modal').classList.remove('flex');
  469. document.getElementById('add-form').reset();
  470. }
  471. // 初始化
  472. loadStats();
  473. loadProducts();
  474. </script>
  475. </body>
  476. </html>