diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 3e95478..8a203c8 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -861,11 +861,64 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{ .toast.show{opacity:1} .overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center} .overlay.hidden{display:none} -.loader{display:flex;gap:8px} -.loader .dot{width:10px;height:10px;border-radius:999px;background:var(--primary);animation:bounce .9s ease infinite} -.loader .dot:nth-child(2){animation-delay:.15s} -.loader .dot:nth-child(3){animation-delay:.3s} -@keyframes bounce{0%,100%{transform:translateY(0);opacity:.7}50%{transform:translateY(-8px);opacity:1}} + +/* 芯片电路板加载动画 */ +.main-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; +} + +.loader { + width: 100%; + max-width: 400px; + height: 100%; + max-height: 250px; +} + +.trace-bg { + stroke: #333; + stroke-width: 1.8; + fill: none; +} + +.trace-flow { + stroke-width: 1.8; + fill: none; + stroke-dasharray: 40 400; + stroke-dashoffset: 438; + filter: drop-shadow(0 0 6px currentColor); + animation: flow 3s cubic-bezier(0.5, 0, 0.9, 1) infinite; +} + +.yellow { + stroke: #ffea00; + color: #ffea00; +} +.blue { + stroke: #00ccff; + color: #00ccff; +} +.green { + stroke: #00ff15; + color: #00ff15; +} +.purple { + stroke: #9900ff; + color: #9900ff; +} +.red { + stroke: #ff3300; + color: #ff3300; +} + +@keyframes flow { + to { + stroke-dashoffset: 0; + } +} .three-body { --uib-size: 35px; @@ -969,65 +1022,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{ opacity: 0.8; } } - - .spinner { - width: 44px; - height: 44px; - position: relative; - perspective: 800px; - animation: spinner-y0fdc1 2s infinite ease; - transform-style: preserve-3d; - } - - .spinner > div { - background-color: rgba(0,77,255,0.2); - height: 100%; - position: absolute; - width: 100%; - border: 2px solid #004dff; - } - - .spinner div:nth-of-type(1) { - transform: translateZ(-22px) rotateY(180deg); - } - - .spinner div:nth-of-type(2) { - transform: rotateY(-270deg) translateX(50%); - transform-origin: top right; - } - - .spinner div:nth-of-type(3) { - transform: rotateY(270deg) translateX(-50%); - transform-origin: center left; - } - - .spinner div:nth-of-type(4) { - transform: rotateX(90deg) translateY(-50%); - transform-origin: top center; - } - - .spinner div:nth-of-type(5) { - transform: rotateX(-90deg) translateY(50%); - transform-origin: bottom center; - } - - .spinner div:nth-of-type(6) { - transform: translateZ(22px); - } - - @keyframes spinner-y0fdc1 { - 0% { - transform: rotate(45deg) rotateX(-25deg) rotateY(25deg); - } - - 50% { - transform: rotate(45deg) rotateX(-385deg) rotateY(25deg); - } - - 100% { - transform: rotate(45deg) rotateX(-385deg) rotateY(385deg); - } - } + .fade-enter{opacity:0;transform:translateY(8px)} .fade-enter-active{transition:opacity .25s ease-out,transform .25s ease-out;opacity:1;transform:translateY(0)} .error{color:#ffb4b4} diff --git a/frontend/index.html b/frontend/index.html index 7784c3a..8772f7b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -279,13 +279,159 @@
- -
- 采购需求计算公式: - 客户订单数量 × BOM单机用量 - 期初库存 = 净需求 → 按最小包装向上取整 = 实际采购数量 -
- +
-
- +
+
@@ -466,6 +515,25 @@
+
+ +
+ + +
+ +
@@ -537,14 +605,20 @@ console.log('检查数据是否有product_code字段:', demandList.some(item => 'product_code' in item)); // 检查字段是否存在 // 更新标签页 - const tabsContainer = document.querySelector('.product-tab').parentElement; + const tabsContainer = document.getElementById('product-tabs-container'); if (!tabsContainer) return; - let html = ''; + const productArray = Array.from(products).sort(); - Array.from(products).sort().forEach(product => { + let html = ``; + + productArray.forEach(product => { const isActive = product === currentProduct ? 'active' : ''; - html += ``; + html += ``; }); tabsContainer.innerHTML = html; @@ -1144,22 +1218,58 @@ const checkboxes = document.querySelectorAll('.row-checkbox:checked'); const selectedCount = checkboxes.length; - if (selectedCount === 0) { - alert('请先选择要编辑的记录'); - return; - } - // 更新选中数量显示 document.getElementById('selected-count').textContent = selectedCount; + // 加载产品列表 + loadProductOptions(); + // 重置表单 document.getElementById('batch-status').value = ''; document.getElementById('batch-remark').value = ''; + document.querySelector('input[name="batch-scope"][value="selected"]').checked = true; + document.getElementById('batch-product-select-container').style.display = 'none'; + + // 添加事件监听 + document.getElementById('batch-scope-all').onchange = function() { + document.getElementById('batch-product-select-container').style.display = + this.checked ? 'block' : 'none'; + }; + document.getElementById('batch-scope-selected').onchange = function() { + document.getElementById('batch-product-select-container').style.display = + this.checked ? 'none' : 'block'; + }; // 显示弹窗 document.getElementById('batch-edit-status-modal').style.display = 'flex'; } + function loadProductOptions() { + // 获取所有产品 + const products = new Set(); + demandList.forEach(item => { + if (item.product_code) { + products.add(item.product_code); + } + }); + + const productSelect = document.getElementById('batch-product-select'); + productSelect.innerHTML = ''; + + // 添加产品选项 + Array.from(products).sort().forEach(product => { + const option = document.createElement('option'); + option.value = product; + option.textContent = product; + productSelect.appendChild(option); + }); + + // 如果当前有选中的产品,设为默认值 + if (currentProduct) { + productSelect.value = currentProduct; + } + } + function closeBatchEditStatusModal() { document.getElementById('batch-edit-status-modal').style.display = 'none'; } @@ -1167,26 +1277,49 @@ async function saveBatchEditStatus() { const status = document.getElementById('batch-status').value; const remark = document.getElementById('batch-remark').value.trim(); + const scope = document.querySelector('input[name="batch-scope"]:checked').value; if (!status) { alert('请选择状态'); return; } - // 获取选中的ID - const checkboxes = document.querySelectorAll('.row-checkbox:checked'); - const ids = Array.from(checkboxes).map(cb => parseInt(cb.dataset.id)); + let requestData = { + status: status, + remark: remark + }; - if (!confirm(`确定要将选中的 ${ids.length} 条记录状态更新为"${getStatusText(status)}"吗?`)) { + let confirmMessage = ''; + + if (scope === 'all') { + // 更新指定产品的所有记录 + const selectedProduct = document.getElementById('batch-product-select').value; + if (!selectedProduct) { + alert('请选择要更新的产品'); + return; + } + requestData.product_code = selectedProduct; + confirmMessage = `确定要将"${selectedProduct}"的所有记录状态更新为"${getStatusText(status)}"吗?`; + } else { + // 仅更新选中的记录 + const checkboxes = document.querySelectorAll('.row-checkbox:checked'); + const ids = Array.from(checkboxes).map(cb => parseInt(cb.dataset.id)); + + if (ids.length === 0) { + alert('请选择要更新的记录'); + return; + } + + requestData.ids = ids; + confirmMessage = `确定要将选中的 ${ids.length} 条记录状态更新为"${getStatusText(status)}"吗?`; + } + + if (!confirm(confirmMessage)) { return; } try { - const res = await API.post('/api/purchase-demand/batch-update-status', { - ids: ids, - status: status, - remark: remark - }); + const res = await API.post('/api/purchase-demand/batch-update-status', requestData); if (res.ok) { alert(`成功更新 ${res.count} 条记录`); diff --git a/server/app.py b/server/app.py index c04e8ca..4501267 100644 --- a/server/app.py +++ b/server/app.py @@ -6507,16 +6507,17 @@ def delete_purchase_demand(demand_id): @app.post('/api/purchase-demand/batch-update-status') @require_login -@require_any_role('superadmin') +@require_any_role('superadmin', 'admin') def batch_update_purchase_demand_status(): """批量更新采购需求状态""" data = request.get_json() or {} ids = data.get('ids', []) - status = data.get('status', '').strip() - remark = data.get('remark', '').strip() + status = data.get('status') + remark = data.get('remark', '') + product_code = data.get('product_code', '').strip() - if not ids: - return jsonify({'error': '请选择要更新的记录'}), 400 + if not ids and not product_code: + return jsonify({'error': '请选择要更新的记录或指定产品'}), 400 if not status: return jsonify({'error': '请选择状态'}), 400 @@ -6528,9 +6529,15 @@ def batch_update_purchase_demand_status(): c = conn.cursor() now = get_beijing_time() - # 验证ID是否存在并获取原始状态 - placeholders = ','.join(['?' for _ in ids]) - c.execute(f'SELECT id, status, material_code, material_name, actual_purchase_qty, net_demand FROM purchase_demand WHERE id IN ({placeholders})', ids) + # 根据条件查询记录 + if product_code: + # 按产品代码查询 + c.execute('SELECT id, status, material_code, material_name, actual_purchase_qty, net_demand, total_demand, initial_stock FROM purchase_demand WHERE product_code=?', (product_code,)) + else: + # 按ID列表查询 + placeholders = ','.join(['?' for _ in ids]) + c.execute(f'SELECT id, status, material_code, material_name, actual_purchase_qty, net_demand, total_demand, initial_stock FROM purchase_demand WHERE id IN ({placeholders})', ids) + existing_records = c.fetchall() if not existing_records: @@ -6547,11 +6554,18 @@ def batch_update_purchase_demand_status(): material_name = record['material_name'] actual_purchase_qty = record['actual_purchase_qty'] net_demand = record['net_demand'] + total_demand = record['total_demand'] + initial_stock = record['initial_stock'] - # 计算之前增加的库存量:实际采购 - 净需求 - added_stock = actual_purchase_qty - net_demand + # 根据实际采购数量计算之前的变化量 + if actual_purchase_qty > 0: + # 如果实际采购不等于零,之前增加的库存量 = 实际采购 - 净需求 + added_stock = actual_purchase_qty - net_demand + else: + # 如果实际采购等于零,之前减少的库存量 = 期初库存 - 总需求 + added_stock = -(initial_stock - total_demand) - # 从期初库存表中减去之前增加的库存(恢复原值) + # 从期初库存表中减去之前的变化量(恢复原值) c.execute('SELECT stock_qty FROM initial_stock WHERE material_code=?', (material_code,)) stock_record = c.fetchone() @@ -6576,24 +6590,30 @@ def batch_update_purchase_demand_status(): params.extend(existing_ids) c.execute(f'UPDATE purchase_demand SET {", ".join(updates)} WHERE id IN ({placeholders})', params) - # 如果状态更新为已下单,更新期初库存 + # 如果状态更新为已下单,更新期初库存(包括已经是已下单状态的情况) if status == 'ordered': for record in existing_records: - # 只处理之前不是"已下单"状态的记录 - if record['status'] != 'ordered': - material_code = record['material_code'] - material_name = record['material_name'] - actual_purchase_qty = record['actual_purchase_qty'] - net_demand = record['net_demand'] - - # 计算新的期初库存:实际采购 - 净需求 + # 处理所有记录(包括已经是"已下单"状态的记录) + material_code = record['material_code'] + material_name = record['material_name'] + actual_purchase_qty = record['actual_purchase_qty'] + net_demand = record['net_demand'] + total_demand = record['total_demand'] + initial_stock = record['initial_stock'] + + # 根据实际采购数量计算新的期初库存 + if actual_purchase_qty > 0: + # 如果实际采购不等于零,期初库存 = 实际采购 - 净需求 new_initial_stock = actual_purchase_qty - net_demand - - # 只更新期初库存表 - c.execute('''INSERT OR REPLACE INTO initial_stock - (material_code, material_name, stock_qty, updated_at) - VALUES (?, ?, ?, ?)''', - (material_code, material_name, new_initial_stock, now)) + else: + # 如果实际采购等于零,期初库存 = 期初库存 - 总需求 + new_initial_stock = initial_stock - total_demand + + # 只更新期初库存表 + c.execute('''INSERT OR REPLACE INTO initial_stock + (material_code, material_name, stock_qty, updated_at) + VALUES (?, ?, ?, ?)''', + (material_code, material_name, new_initial_stock, now)) count = c.rowcount conn.commit() @@ -6881,11 +6901,18 @@ def update_purchase_demand(id): material_name = existing['material_name'] actual_purchase_qty = existing['actual_purchase_qty'] net_demand = existing['net_demand'] + total_demand = existing['total_demand'] + initial_stock = existing['initial_stock'] - # 计算之前增加的库存量:实际采购 - 净需求 - added_stock = actual_purchase_qty - net_demand + # 根据实际采购数量计算之前的变化量 + if actual_purchase_qty > 0: + # 如果实际采购不等于零,之前增加的库存量 = 实际采购 - 净需求 + added_stock = actual_purchase_qty - net_demand + else: + # 如果实际采购等于零,之前减少的库存量 = 期初库存 - 总需求 + added_stock = -(initial_stock - total_demand) - # 从期初库存表中减去之前增加的库存(恢复原值) + # 从期初库存表中减去之前的变化量(恢复原值) c.execute('SELECT stock_qty FROM initial_stock WHERE material_code=?', (material_code,)) stock_record = c.fetchone() @@ -6903,16 +6930,22 @@ def update_purchase_demand(id): update_values.append(id) c.execute(f'UPDATE purchase_demand SET {", ".join(update_fields)} WHERE id=?', update_values) - # 如果状态更新为已下单,更新期初库存 - if 'status' in data and new_status == 'ordered' and old_status != 'ordered': + # 如果状态更新为已下单,更新期初库存(包括已经是已下单状态的情况) + if 'status' in data and new_status == 'ordered': # 获取更新后的记录信息 - c.execute('SELECT material_code, material_name, actual_purchase_qty, net_demand FROM purchase_demand WHERE id=?', (id,)) + c.execute('SELECT material_code, material_name, actual_purchase_qty, net_demand, total_demand, initial_stock FROM purchase_demand WHERE id=?', (id,)) record = c.fetchone() if record: - material_code, material_name, actual_purchase_qty, net_demand = record - # 计算新的期初库存:实际采购 - 净需求 - new_initial_stock = actual_purchase_qty - net_demand + material_code, material_name, actual_purchase_qty, net_demand, total_demand, initial_stock = record + + # 根据实际采购数量计算新的期初库存 + if actual_purchase_qty > 0: + # 如果实际采购不等于零,期初库存 = 实际采购 - 净需求 + new_initial_stock = actual_purchase_qty - net_demand + else: + # 如果实际采购等于零,期初库存 = 期初库存 - 总需求 + new_initial_stock = initial_stock - total_demand # 只更新期初库存表 c.execute('''INSERT OR REPLACE INTO initial_stock