diff --git a/batch_import.py b/batch_import.py deleted file mode 100644 index 6b302a9..0000000 --- a/batch_import.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -import pandas as pd -import redis -from tqdm import tqdm -import argparse -import os - -# 连接Redis -parser = argparse.ArgumentParser() -parser.add_argument("type", choices=["pdd", "yt", "tx"], help="目标: pdd/yt/tx") -args = parser.parse_args() - -r = redis.Redis(host='180.163.74.83', port=6379, password='Zzh08165511', decode_responses=True) - -# 读取Excel文件 -if args.type == "yt": - excel_path = '/home/hyx/work/batch_import_xlsx/sn_test_yt.xlsx' - pool = 'batch_sn_mapping_yt' - mac_col = 'MAC' -elif args.type == "pdd": - excel_path = '/home/hyx/work/batch_import_xlsx/sn_test_pdd.xlsx' - pool = 'batch_sn_mapping_pdd' - mac_col = 'MAC' -else: - excel_path = '/home/hyx/work/batch_import_xlsx/sn_test_tx.xlsx' - pool = 'batch_sn_mapping' - mac_col = 'SN_MAC' - -df = pd.read_excel(excel_path) -existing = r.hgetall(pool) -mac_to_batches = {} -for b, m in existing.items(): - mac_to_batches.setdefault(m, []).append(b) -s = df[mac_col].astype(str).str.strip() -dup_keys = set(s[s.duplicated(keep=False)].unique()) - -# 批量导入数据 -pipe = r.pipeline() -duplicates = [] -inserted_count = 0 -invalids = [] -duplicates_current = {} -dup_current_count = 0 -for index, row in tqdm(df.iterrows(), total=len(df)): - batch_no = str(row['批次号']).strip() - sn_mac = str(row[mac_col]).strip() - expected_len = 27 if args.type == 'tx' else 12 - - if len(sn_mac) != expected_len: - invalids.append((sn_mac, batch_no)) - continue - - if sn_mac in dup_keys: - s = duplicates_current.get(sn_mac, set()) - s.add(batch_no) - duplicates_current[sn_mac] = s - dup_current_count += 1 - continue - - if sn_mac in mac_to_batches: - for b in mac_to_batches[sn_mac]: - duplicates.append((sn_mac, b)) - continue - - pipe.hset(pool, batch_no, sn_mac) - inserted_count += 1 - - if (index + 1) % 100 == 0: - pipe.execute() - pipe = r.pipeline() - -pipe.execute() -print(f"成功导入 {inserted_count} 条数据,数据库重复跳过 {len(duplicates)} 条,当前批次重复跳过 {dup_current_count} 条,长度错误跳过 {len(invalids)} 条") - -# 输出成功导入的数据(JSON格式,方便前端解析) -if inserted_count > 0: - print("\n=== 成功导入的数据 ===") - import json - success_records = [] - for index, row in df.iterrows(): - batch_no = str(row['批次号']).strip() - sn_mac = str(row[mac_col]).strip() - expected_len = 27 if args.type == 'tx' else 12 - - # 只输出成功导入的记录 - if len(sn_mac) == expected_len and sn_mac not in dup_keys and sn_mac not in mac_to_batches: - success_records.append({ - 'mac': sn_mac, - 'batch': batch_no - }) - # 移除数量限制,输出所有成功导入的记录 - - print(json.dumps(success_records, ensure_ascii=False)) - print("=== 数据输出结束 ===") -if duplicates: - for mac, b in duplicates: - print(f"重复: {mac} 已存在于批次号 {b}") - dup_df = pd.DataFrame(duplicates, columns=[mac_col, '批次号']) - out_path = f"/home/hyx/work/batch_import_xlsx/duplicates_{args.type}.xlsx" - if os.path.exists(out_path): - old_df = pd.read_excel(out_path) - combined = pd.concat([old_df, dup_df], ignore_index=True) - combined.to_excel(out_path, index=False) - else: - dup_df.to_excel(out_path, index=False) - #print(f"重复数据已导出: {out_path}") -if duplicates_current: - for mac, bs in duplicates_current.items(): - for b in bs: - print(f"重复: {mac} 当前批次号 {b}") - cur_rows = [(mac, b) for mac, bs in duplicates_current.items() for b in bs] - cur_dup_df = pd.DataFrame(cur_rows, columns=[mac_col, '批次号']) - out_path_cur = f"/home/hyx/work/batch_import_xlsx/duplicates_current_{args.type}.xlsx" - if os.path.exists(out_path_cur): - old_cur_df = pd.read_excel(out_path_cur) - combined_cur = pd.concat([old_cur_df, cur_dup_df], ignore_index=True) - combined_cur.to_excel(out_path_cur, index=False) - else: - cur_dup_df.to_excel(out_path_cur, index=False) - #print(f"当前批次重复数据已导出: {out_path_cur}") -if invalids: - for mac, b in invalids: - print(f"长度错误: {mac} 批次号 {b}") - inv_df = pd.DataFrame(invalids, columns=[mac_col, '批次号']) - out_path_inv = f"/home/hyx/work/batch_import_xlsx/invalid_{args.type}.xlsx" - if os.path.exists(out_path_inv): - old_inv_df = pd.read_excel(out_path_inv) - combined_inv = pd.concat([old_inv_df, inv_df], ignore_index=True) - combined_inv.to_excel(out_path_inv, index=False) - else: - inv_df.to_excel(out_path_inv, index=False) - #print(f"长度错误数据已导出: {out_path_inv}") diff --git a/check_excel.py b/check_excel.py deleted file mode 100644 index 6b110a4..0000000 --- a/check_excel.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -import pandas as pd -import openpyxl -import warnings - -# 过滤openpyxl的跨平台兼容性警告 -warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl') - -file_path = '/home/hyx/work/batch_import_xlsx/sn_test_tx.xlsx' - -print("检查Excel文件信息...") - -try: - # 使用openpyxl检查工作表(兼容Windows到Mac的Excel文件) - wb = openpyxl.load_workbook(file_path, data_only=True) - print(f"工作表数量: {len(wb.sheetnames)}") - print(f"工作表名称: {wb.sheetnames}") - - if wb.sheetnames: - ws = wb.active - print(f"活动工作表: {ws.title}") - print(f"最大行数: {ws.max_row}") - print(f"最大列数: {ws.max_column}") - - # 显示前几行数据 - print("\n前10行数据:") - for i, row in enumerate(ws.iter_rows(values_only=True), 1): - if i <= 10: - print(f"第{i}行: {row}") - else: - break - - wb.close() # 关闭工作簿释放资源 - -except Exception as e: - print(f"openpyxl错误: {e}") - print("提示: 这可能是Windows到Mac的Excel文件兼容性问题") - -try: - # 使用pandas检查 - print("\n使用pandas检查...") - xl_file = pd.ExcelFile(file_path) - print(f"pandas检测到的工作表: {xl_file.sheet_names}") - -except Exception as e: - print(f"pandas错误: {e}") diff --git a/create_shipments_template.py b/create_shipments_template.py deleted file mode 100644 index d152530..0000000 --- a/create_shipments_template.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -""" -创建发货记录 Excel 模板文件 -""" -import pandas as pd -from datetime import datetime, timedelta - -def create_template(): - """创建发货记录模板文件(带合并单元格)""" - import openpyxl - from openpyxl.styles import Alignment, Font, Border, Side - - # 创建工作簿 - wb = openpyxl.Workbook() - ws = wb.active - ws.title = "发货记录" - - # 创建表头 - headers = ['出货日期', '箱号'] - headers.extend([f'SN{i}' for i in range(1, 21)]) - ws.append(headers) - - # 设置表头样式 - for cell in ws[1]: - cell.font = Font(bold=True) - cell.alignment = Alignment(horizontal='center', vertical='center') - - # 创建示例数据 - base_date = datetime.now() - row_num = 2 - - # 每个日期3个箱子 - for day in range(3): - date = (base_date + timedelta(days=day)).strftime('%Y-%m-%d') - start_row = row_num - - # 每天3个箱子 - for box in range(3): - box_num = f"BOX{day*3+box+1:03d}" - - # 第一列:日期(只在第一行写入,后面会合并) - if box == 0: - ws.cell(row=row_num, column=1, value=date) - - # 第二列:箱号 - ws.cell(row=row_num, column=2, value=box_num) - - # SN1-SN20 - for sn_idx in range(1, 21): - sn_value = f"SN{(day*3+box)*20+sn_idx:04d}" if sn_idx <= 15 else '' - ws.cell(row=row_num, column=2+sn_idx, value=sn_value) - - row_num += 1 - - # 合并日期单元格 - if start_row < row_num - 1: - ws.merge_cells(f'A{start_row}:A{row_num-1}') - # 设置合并单元格的对齐方式 - ws.cell(row=start_row, column=1).alignment = Alignment(horizontal='center', vertical='center') - - # 调整列宽 - ws.column_dimensions['A'].width = 12 - ws.column_dimensions['B'].width = 10 - for i in range(3, 23): - ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = 10 - - # 保存文件 - output_file = 'shipments_template.xlsx' - wb.save(output_file) - - print(f"✓ 模板文件已创建:{output_file}") - print(f" - 包含 {row_num-2} 行示例数据") - print(f" - 列:出货日期(合并单元格)、箱号、SN1-SN20") - print(f" - 每个日期包含 3 个箱子") - - return output_file - -def create_empty_template(): - """创建空白模板文件""" - - # 创建列头 - columns = ['出货日期', '箱号'] - columns.extend([f'SN{i}' for i in range(1, 21)]) - - # 创建空 DataFrame - df = pd.DataFrame(columns=columns) - - # 保存为 Excel - output_file = 'shipments_template_empty.xlsx' - df.to_excel(output_file, index=False, engine='openpyxl') - - print(f"✓ 空白模板文件已创建:{output_file}") - - return output_file - -if __name__ == '__main__': - print("创建发货记录 Excel 模板...\n") - - # 创建带示例数据的模板 - create_template() - print() - - # 创建空白模板 - create_empty_template() - print() - - print("完成!您可以使用这些模板文件进行测试。") diff --git a/frontend/assets/login.css b/frontend/assets/login.css new file mode 100644 index 0000000..f30b076 --- /dev/null +++ b/frontend/assets/login.css @@ -0,0 +1,428 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; + line-height: 1.5; + overflow: hidden; +} + +.login-container { + position: relative; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +/* 背景层 - 工业风格渐变 + 几何图案 */ +.login-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + linear-gradient(135deg, #1e3a8a 0%, #1e40af 25%, #3b82f6 50%, #60a5fa 75%, #93c5fd 100%); + z-index: 0; +} + +.login-background::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(255, 255, 255, 0.03) 2px, + rgba(255, 255, 255, 0.03) 4px + ), + repeating-linear-gradient( + 90deg, + transparent, + transparent 2px, + rgba(255, 255, 255, 0.03) 2px, + rgba(255, 255, 255, 0.03) 4px + ); + background-size: 50px 50px; +} + +.login-background::after { + content: ''; + position: absolute; + top: -50%; + right: -20%; + width: 80%; + height: 80%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + border-radius: 50%; + animation: float 20s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(-50px, 50px) scale(1.1); + } +} + +/* 登录卡片 */ +.login-card { + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-radius: 24px; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.1) inset; + padding: 48px; + width: 90%; + max-width: 440px; + animation: slideUp 0.6s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 登录头部 */ +.login-header { + text-align: center; + margin-bottom: 40px; +} + +.logo-container { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 8px; +} + +.logo-icon { + width: 64px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + animation: float-icon 3s ease-in-out infinite; +} + +.logo-icon svg { + width: 100%; + height: 100%; + filter: drop-shadow(0 4px 8px rgba(59, 130, 246, 0.3)); +} + +@keyframes float-icon { + 0%, 100% { + transform: translateY(0) scale(1); + } + 50% { + transform: translateY(-5px) scale(1.02); + } +} + +.system-title { + font-size: 28px; + font-weight: 700; + color: #1e3a8a; + margin: 0; +} + +.system-subtitle { + font-size: 13px; + color: #64748b; + letter-spacing: 1px; + text-transform: uppercase; + margin: 0; +} + +/* 表单 */ +.login-form { + margin-bottom: 32px; +} + +.form-group { + margin-bottom: 24px; +} + +.form-group label { + display: block; + font-size: 14px; + font-weight: 600; + color: #334155; + margin-bottom: 8px; +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.input-icon { + position: absolute; + left: 16px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + z-index: 1; + color: #64748b; + transition: all 0.3s ease; +} + +.input-icon svg { + width: 100%; + height: 100%; +} + +.form-input { + width: 100%; + padding: 14px 16px 14px 48px; + font-size: 15px; + font-family: inherit; + color: #1e293b; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + transition: all 0.3s ease; + outline: none; +} + +.form-input::placeholder { + color: #94a3b8; +} + +.form-input:focus { + background: #ffffff; + border-color: #3b82f6; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); +} + +.input-wrapper:focus-within .input-icon { + color: #3b82f6; + transform: scale(1.1); +} + +/* 验证码 */ +.captcha-wrapper { + display: flex; + gap: 12px; + align-items: center; +} + +.captcha-image-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 120px; + height: 48px; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; + flex-shrink: 0; + cursor: pointer; + transition: all 0.3s ease; +} + +.captcha-image-wrapper:hover { + border-color: #3b82f6; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); + transform: scale(1.02); +} + +.captcha-image-wrapper:active { + transform: scale(0.98); +} + +.captcha-image-wrapper.refreshing { + animation: rotate 0.3s ease; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(180deg); + } +} + +.captcha-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + pointer-events: none; +} + +/* 错误消息 */ +.error-message { + background: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 12px 16px; + border-radius: 10px; + font-size: 14px; + margin-bottom: 20px; + animation: shake 0.4s ease; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-10px); } + 75% { transform: translateX(10px); } +} + +/* 登录按钮 */ +.login-btn { + width: 100%; + padding: 16px; + font-size: 16px; + font-weight: 600; + font-family: inherit; + color: #ffffff; + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + border: none; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + position: relative; + overflow: hidden; +} + +.login-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s ease; +} + +.login-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4); +} + +.login-btn:hover::before { + left: 100%; +} + +.login-btn:active { + transform: translateY(0); +} + +.login-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.btn-loader { + display: flex; + gap: 6px; + justify-content: center; + align-items: center; +} + +.btn-loader .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #ffffff; + animation: bounce 0.9s ease infinite; +} + +.btn-loader .dot:nth-child(2) { + animation-delay: 0.15s; +} + +.btn-loader .dot:nth-child(3) { + animation-delay: 0.3s; +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + opacity: 0.7; + } + 50% { + transform: translateY(-8px); + opacity: 1; + } +} + +/* 页脚 */ +.login-footer { + text-align: center; + padding-top: 24px; + border-top: 1px solid #e2e8f0; +} + +.login-footer p { + font-size: 13px; + color: #64748b; + margin: 0; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .login-card { + padding: 32px 24px; + max-width: 100%; + margin: 20px; + } + + .system-title { + font-size: 24px; + } + + .logo-icon { + font-size: 40px; + } +} + +@media (max-width: 480px) { + .login-card { + padding: 24px 20px; + } + + .system-title { + font-size: 20px; + } + + .logo-container { + flex-direction: column; + gap: 8px; + } +} diff --git a/frontend/js/components/material-purchase.js b/frontend/js/components/material-purchase.js new file mode 100644 index 0000000..31af01d --- /dev/null +++ b/frontend/js/components/material-purchase.js @@ -0,0 +1,1320 @@ +(() => { + Router.register('/plan-mgmt/material-purchase', async () => { + return ` +
+ + +
+
+ + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 标题生产计划明细物料需求清单编号生产计划编号产品BOM分析结果状态需求状态物料齐套率物料编码物料名称物料批次号物料层级所需物料数库存现有物料数欠缺值物料获取方式实时库存值待入库数量派发数量入库数量提交人提交时间更新时间操作
+
暂无数据
+
+
+ + +
+ + + + + + + + + + + + + `; + }); + + window.MaterialPurchase = { + currentPage: 1, + pageSize: 20, + totalPages: 1, + data: [], + editingId: null, + selectedIds: new Set(), + recycleBin: [], + + init() { + this.loadFromStorage(); + this.loadData(); + this.bindEvents(); + }, + + // 从 localStorage 加载数据 + loadFromStorage() { + try { + const savedData = localStorage.getItem('materialPurchaseData'); + const savedRecycleBin = localStorage.getItem('materialPurchaseRecycleBin'); + + if (savedData) { + this.data = JSON.parse(savedData); + } + if (savedRecycleBin) { + this.recycleBin = JSON.parse(savedRecycleBin); + } + } catch (error) { + console.error('加载本地数据失败:', error); + } + }, + + // 保存到 localStorage + saveToStorage() { + try { + localStorage.setItem('materialPurchaseData', JSON.stringify(this.data)); + localStorage.setItem('materialPurchaseRecycleBin', JSON.stringify(this.recycleBin)); + } catch (error) { + console.error('保存本地数据失败:', error); + } + }, + + bindEvents() { + // 自动计算欠缺值 + const requiredQty = document.getElementById('form-required-qty'); + const stockQty = document.getElementById('form-stock-qty'); + const shortage = document.getElementById('form-shortage'); + + const calculateShortage = () => { + const required = parseFloat(requiredQty?.value || 0); + const stock = parseFloat(stockQty?.value || 0); + if (shortage) { + shortage.value = Math.max(0, required - stock); + } + }; + + requiredQty?.addEventListener('input', calculateShortage); + stockQty?.addEventListener('input', calculateShortage); + }, + + async loadData() { + try { + const response = await fetch('/api/material-purchase/list', { + credentials: 'include' + }); + const result = await response.json(); + this.data = result.list || []; + + // 加载回收站数据 + const recycleBinResponse = await fetch('/api/material-purchase/recycle-bin', { + credentials: 'include' + }); + const recycleBinResult = await recycleBinResponse.json(); + this.recycleBin = recycleBinResult.list || []; + + this.renderTable(); + this.updatePagination(); + } catch (error) { + API.toast('加载数据失败: ' + error.message, 'error'); + } + }, + + generateMockData() { + // 生成模拟数据 + const mockData = []; + for (let i = 1; i <= 5; i++) { + mockData.push({ + id: i, + title: `物料需求计划-${i}`, + listNo: `MRL-2024-${String(i).padStart(4, '0')}`, + planNo: `PLAN-2024-${String(i).padStart(4, '0')}`, + bomResult: `BOM分析-${i}`, + status: ['pending', 'processing', 'completed'][i % 3], + demandStatus: ['urgent', 'normal', 'low'][i % 3], + completeRate: (Math.random() * 100).toFixed(2), + materialCode: `MAT-${String(i).padStart(5, '0')}`, + materialName: `物料名称-${i}`, + batchNo: `BATCH-${String(i).padStart(4, '0')}`, + level: Math.floor(Math.random() * 3) + 1, + requiredQty: Math.floor(Math.random() * 1000) + 100, + stockQty: Math.floor(Math.random() * 500), + shortage: 0, + acquireMethod: ['purchase', 'production', 'transfer'][i % 3], + realtimeStock: Math.floor(Math.random() * 500), + pendingQty: Math.floor(Math.random() * 200), + dispatchedQty: Math.floor(Math.random() * 100), + receivedQty: Math.floor(Math.random() * 150), + submitter: `用户${i}`, + submitTime: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(), + updateTime: new Date().toISOString() + }); + mockData[i - 1].shortage = Math.max(0, mockData[i - 1].requiredQty - mockData[i - 1].stockQty); + } + return mockData; + }, + + renderTable() { + const tbody = document.getElementById('material-tbody'); + if (!tbody) return; + + if (this.data.length === 0) { + tbody.innerHTML = ` + + +
暂无数据
+ + + `; + return; + } + + const start = (this.currentPage - 1) * this.pageSize; + const end = start + this.pageSize; + const pageData = this.data.slice(start, end); + + tbody.innerHTML = pageData.map(item => ` + + + + + ${this.escapeHtml(item.title)} + ${this.escapeHtml(item.listNo)} + ${this.escapeHtml(item.planNo)} + ${this.escapeHtml(item.bomResult)} + ${this.getStatusText(item.status)} + ${this.getDemandStatusText(item.demandStatus)} + ${item.completeRate}% + ${this.escapeHtml(item.materialCode)} + ${this.escapeHtml(item.materialName)} + ${this.escapeHtml(item.batchNo)} + ${item.level} + ${item.requiredQty} + ${item.stockQty} + ${item.shortage} + ${this.getAcquireMethodText(item.acquireMethod)} + ${item.realtimeStock} + ${item.pendingQty} + ${item.dispatchedQty} + ${item.receivedQty} + ${this.escapeHtml(item.submitter)} + ${this.formatDateTime(item.submitTime)} + ${this.formatDateTime(item.updateTime)} + + + + + + `).join(''); + + // 更新全选框状态 + this.updateSelectAllCheckbox(); + }, + + updatePagination() { + this.totalPages = Math.ceil(this.data.length / this.pageSize); + const pageInfo = document.getElementById('page-info'); + const prevBtn = document.getElementById('prev-btn'); + const nextBtn = document.getElementById('next-btn'); + + if (pageInfo) { + pageInfo.textContent = `第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`; + } + if (prevBtn) { + prevBtn.disabled = this.currentPage === 1; + } + if (nextBtn) { + nextBtn.disabled = this.currentPage === this.totalPages || this.totalPages === 0; + } + }, + + prevPage() { + if (this.currentPage > 1) { + this.currentPage--; + this.renderTable(); + this.updatePagination(); + } + }, + + nextPage() { + if (this.currentPage < this.totalPages) { + this.currentPage++; + this.renderTable(); + this.updatePagination(); + } + }, + + search() { + const keyword = document.getElementById('search-keyword')?.value.toLowerCase() || ''; + const status = document.getElementById('filter-status')?.value || ''; + const demandStatus = document.getElementById('filter-demand-status')?.value || ''; + + this.data = this.generateMockData().filter(item => { + const matchKeyword = !keyword || + item.title.toLowerCase().includes(keyword) || + item.listNo.toLowerCase().includes(keyword) || + item.planNo.toLowerCase().includes(keyword) || + item.materialCode.toLowerCase().includes(keyword); + + const matchStatus = !status || item.status === status; + const matchDemandStatus = !demandStatus || item.demandStatus === demandStatus; + + return matchKeyword && matchStatus && matchDemandStatus; + }); + + this.currentPage = 1; + this.renderTable(); + this.updatePagination(); + }, + + resetFilter() { + document.getElementById('search-keyword').value = ''; + document.getElementById('filter-status').value = ''; + document.getElementById('filter-demand-status').value = ''; + this.loadData(); + }, + + showAddDialog() { + this.editingId = null; + document.getElementById('dialog-title').textContent = '新增物料需求'; + document.getElementById('material-form').reset(); + document.getElementById('material-dialog').style.display = 'flex'; + }, + + editItem(id) { + this.editingId = id; + const item = this.data.find(d => d.id === id); + if (!item) return; + + document.getElementById('dialog-title').textContent = '编辑物料需求'; + document.getElementById('form-title').value = item.title; + document.getElementById('form-list-no').value = item.listNo; + document.getElementById('form-plan-no').value = item.planNo; + document.getElementById('form-bom-result').value = item.bomResult; + document.getElementById('form-status').value = item.status; + document.getElementById('form-demand-status').value = item.demandStatus; + document.getElementById('form-complete-rate').value = item.completeRate; + document.getElementById('form-material-code').value = item.materialCode; + document.getElementById('form-material-name').value = item.materialName; + document.getElementById('form-batch-no').value = item.batchNo; + document.getElementById('form-level').value = item.level; + document.getElementById('form-required-qty').value = item.requiredQty; + document.getElementById('form-stock-qty').value = item.stockQty; + document.getElementById('form-shortage').value = item.shortage; + document.getElementById('form-acquire-method').value = item.acquireMethod; + document.getElementById('form-realtime-stock').value = item.realtimeStock; + document.getElementById('form-pending-qty').value = item.pendingQty; + document.getElementById('form-dispatched-qty').value = item.dispatchedQty; + document.getElementById('form-received-qty').value = item.receivedQty; + + document.getElementById('material-dialog').style.display = 'flex'; + }, + + closeDialog() { + document.getElementById('material-dialog').style.display = 'none'; + this.editingId = null; + }, + + async saveData() { + const form = document.getElementById('material-form'); + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const data = { + title: document.getElementById('form-title').value, + list_no: document.getElementById('form-list-no').value, + plan_no: document.getElementById('form-plan-no').value, + bom_result: document.getElementById('form-bom-result').value, + status: document.getElementById('form-status').value, + demand_status: document.getElementById('form-demand-status').value, + complete_rate: parseFloat(document.getElementById('form-complete-rate').value) || 0, + material_code: document.getElementById('form-material-code').value, + material_name: document.getElementById('form-material-name').value, + batch_no: document.getElementById('form-batch-no').value, + level: parseInt(document.getElementById('form-level').value) || 1, + required_qty: parseInt(document.getElementById('form-required-qty').value) || 0, + stock_qty: parseInt(document.getElementById('form-stock-qty').value) || 0, + shortage: parseInt(document.getElementById('form-shortage').value) || 0, + acquire_method: document.getElementById('form-acquire-method').value, + realtime_stock: parseInt(document.getElementById('form-realtime-stock').value) || 0, + pending_qty: parseInt(document.getElementById('form-pending-qty').value) || 0, + dispatched_qty: parseInt(document.getElementById('form-dispatched-qty').value) || 0, + received_qty: parseInt(document.getElementById('form-received-qty').value) || 0 + }; + + try { + const url = this.editingId ? '/api/material-purchase/update' : '/api/material-purchase/add'; + if (this.editingId) { + data.id = this.editingId; + } + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(data) + }); + + const result = await response.json(); + if (result.ok) { + API.toast(this.editingId ? '更新成功' : '添加成功', 'success'); + this.closeDialog(); + await this.loadData(); + } else { + API.toast(result.error || '保存失败', 'error'); + } + } catch (error) { + API.toast('保存失败: ' + error.message, 'error'); + } + }, + + async deleteItem(id) { + if (!confirm('确定要删除这条记录吗?\n\n删除的数据将移至回收站,可在回收站中找回。')) return; + + try { + const response = await fetch('/api/material-purchase/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ ids: [id] }) + }); + + const result = await response.json(); + if (result.ok) { + API.toast('已删除,可在回收站中找回', 'success'); + await this.loadData(); + } else { + API.toast(result.error || '删除失败', 'error'); + } + } catch (error) { + API.toast('删除失败: ' + error.message, 'error'); + } + }, + + exportData() { + this.showExportDialog(); + }, + + showExportDialog() { + const dialog = document.getElementById('export-dialog'); + if (dialog) { + dialog.style.display = 'flex'; + // 默认全选 + document.querySelectorAll('.export-field-checkbox').forEach(cb => { + cb.checked = true; + }); + } + }, + + closeExportDialog() { + const dialog = document.getElementById('export-dialog'); + if (dialog) { + dialog.style.display = 'none'; + } + }, + + async performExport() { + // 获取选中的字段 + const selectedFields = []; + document.querySelectorAll('.export-field-checkbox:checked').forEach(cb => { + selectedFields.push({ + key: cb.value, + label: cb.dataset.label + }); + }); + + if (selectedFields.length === 0) { + API.toast('请至少选择一个导出字段', 'warning'); + return; + } + + try { + // 动态加载 SheetJS 库 + if (!window.XLSX) { + API.toast('正在加载导出库...', 'info'); + await this.loadSheetJS(); + } + + // 准备导出数据 + const exportData = this.data.map(item => { + const row = {}; + selectedFields.forEach(field => { + let value = item[field.key]; + + // 格式化特殊字段 + if (field.key === 'status') { + value = this.getStatusText(value); + } else if (field.key === 'demandStatus') { + value = this.getDemandStatusText(value); + } else if (field.key === 'acquireMethod') { + value = this.getAcquireMethodText(value); + } else if (field.key === 'submitTime' || field.key === 'updateTime') { + value = this.formatDateTime(value); + } else if (field.key === 'completeRate') { + value = value + '%'; + } + + row[field.label] = value || ''; + }); + return row; + }); + + // 创建工作簿 + const ws = window.XLSX.utils.json_to_sheet(exportData); + const wb = window.XLSX.utils.book_new(); + window.XLSX.utils.book_append_sheet(wb, ws, '物料清单'); + + // 设置列宽 + const colWidths = selectedFields.map(field => { + let width = 15; + if (field.key === 'listNo' || field.key === 'planNo') width = 20; + if (field.key === 'title' || field.key === 'materialName') width = 25; + if (field.key === 'submitTime' || field.key === 'updateTime') width = 20; + return { wch: width }; + }); + ws['!cols'] = colWidths; + + // 生成文件名 + const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); + const filename = `物料清单-采购_${timestamp}.xlsx`; + + // 导出文件 + window.XLSX.writeFile(wb, filename); + + API.toast('导出成功!', 'success'); + this.closeExportDialog(); + } catch (error) { + console.error('导出失败:', error); + API.toast('导出失败: ' + error.message, 'error'); + } + }, + + async loadSheetJS() { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js'; + script.onload = resolve; + script.onerror = () => reject(new Error('加载导出库失败')); + document.head.appendChild(script); + }); + }, + + selectAllFields() { + document.querySelectorAll('.export-field-checkbox').forEach(cb => { + cb.checked = true; + }); + }, + + deselectAllFields() { + document.querySelectorAll('.export-field-checkbox').forEach(cb => { + cb.checked = false; + }); + }, + + toggleRowSelection(id, checked) { + if (checked) { + this.selectedIds.add(id); + } else { + this.selectedIds.delete(id); + } + this.updateSelectAllCheckbox(); + this.updateSelectionInfo(); + }, + + toggleSelectAll(checkbox) { + const checkboxes = document.querySelectorAll('.row-checkbox'); + checkboxes.forEach(cb => { + const id = parseInt(cb.value); + cb.checked = checkbox.checked; + if (checkbox.checked) { + this.selectedIds.add(id); + } else { + this.selectedIds.delete(id); + } + }); + this.updateSelectionInfo(); + }, + + updateSelectAllCheckbox() { + const selectAllCheckbox = document.getElementById('select-all-checkbox'); + const checkboxes = document.querySelectorAll('.row-checkbox'); + const checkedCount = document.querySelectorAll('.row-checkbox:checked').length; + + if (selectAllCheckbox) { + selectAllCheckbox.checked = checkboxes.length > 0 && checkedCount === checkboxes.length; + selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length; + } + }, + + updateSelectionInfo() { + const count = this.selectedIds.size; + const infoEl = document.getElementById('selection-info'); + if (infoEl) { + if (count > 0) { + infoEl.textContent = `已选择 ${count} 项`; + infoEl.style.display = 'inline-block'; + } else { + infoEl.style.display = 'none'; + } + } + }, + + clearSelection() { + this.selectedIds.clear(); + document.querySelectorAll('.row-checkbox').forEach(cb => { + cb.checked = false; + }); + const selectAllCheckbox = document.getElementById('select-all-checkbox'); + if (selectAllCheckbox) { + selectAllCheckbox.checked = false; + selectAllCheckbox.indeterminate = false; + } + this.updateSelectionInfo(); + }, + + async deleteSelected() { + if (this.selectedIds.size === 0) { + API.toast('请先选择要删除的项', 'warning'); + return; + } + + if (!confirm(`确定要删除选中的 ${this.selectedIds.size} 项吗?\n\n删除的数据将移至回收站,可在回收站中找回。`)) { + return; + } + + try { + const ids = Array.from(this.selectedIds); + const response = await fetch('/api/material-purchase/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ ids }) + }); + + const result = await response.json(); + if (result.ok) { + API.toast(`已删除 ${result.count} 项,可在回收站中找回`, 'success'); + this.selectedIds.clear(); + await this.loadData(); + this.updateSelectionInfo(); + } else { + API.toast(result.error || '删除失败', 'error'); + } + } catch (error) { + API.toast('删除失败: ' + error.message, 'error'); + } + }, + + exportSelected() { + if (this.selectedIds.size === 0) { + API.toast('请先选择要导出的项', 'warning'); + return; + } + + // 临时保存原始数据 + const originalData = this.data; + // 只导出选中的数据 + this.data = this.data.filter(item => this.selectedIds.has(item.id)); + + // 显示导出对话框 + this.showExportDialog(); + + // 恢复原始数据 + setTimeout(() => { + this.data = originalData; + }, 100); + }, + + getStatusClass(status) { + const map = { + pending: 'warning', + processing: 'info', + completed: 'success' + }; + return map[status] || 'default'; + }, + + getStatusText(status) { + const map = { + pending: '待处理', + processing: '处理中', + completed: '已完成' + }; + return map[status] || status; + }, + + getDemandStatusClass(status) { + const map = { + urgent: 'danger', + normal: 'info', + low: 'default' + }; + return map[status] || 'default'; + }, + + getDemandStatusText(status) { + const map = { + urgent: '紧急', + normal: '正常', + low: '低优先级' + }; + return map[status] || status; + }, + + getAcquireMethodText(method) { + if (!method) return '-'; + + const map = { + purchase: '采购', + production: '生产', + transfer: '调拨', + // 支持中文输入 + '采购': '采购', + '生产': '生产', + '调拨': '调拨' + }; + return map[method] || method || '-'; + }, + + formatDateTime(dateStr) { + if (!dateStr) return '-'; + const date = new Date(dateStr); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + }, + + escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + // 回收站相关功能 + showRecycleBin() { + const dialog = document.getElementById('recycle-bin-dialog'); + if (dialog) { + this.renderRecycleBin(); + dialog.style.display = 'flex'; + } + }, + + closeRecycleBin() { + const dialog = document.getElementById('recycle-bin-dialog'); + if (dialog) { + dialog.style.display = 'none'; + } + }, + + renderRecycleBin() { + const tbody = document.getElementById('recycle-bin-tbody'); + if (!tbody) return; + + if (this.recycleBin.length === 0) { + tbody.innerHTML = ` + + +
回收站为空
+ + + `; + return; + } + + tbody.innerHTML = this.recycleBin.map(item => ` + + ${this.escapeHtml(item.title)} + ${this.escapeHtml(item.listNo)} + ${this.escapeHtml(item.materialCode)} + ${this.escapeHtml(item.materialName)} + ${this.formatDateTime(item.deletedAt)} + + + + + + `).join(''); + }, + + async restoreItem(id) { + try { + const response = await fetch('/api/material-purchase/restore', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ id }) + }); + + const result = await response.json(); + if (result.ok) { + API.toast('恢复成功', 'success'); + await this.loadData(); + this.renderRecycleBin(); + } else { + API.toast(result.error || '恢复失败', 'error'); + } + } catch (error) { + API.toast('恢复失败: ' + error.message, 'error'); + } + }, + + async permanentDelete(id) { + if (!confirm('确定要永久删除这条记录吗?此操作不可恢复!')) { + return; + } + + try { + const response = await fetch('/api/material-purchase/permanent-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ id }) + }); + + const result = await response.json(); + if (result.ok) { + API.toast('已永久删除', 'success'); + await this.loadData(); + this.renderRecycleBin(); + } else { + API.toast(result.error || '删除失败', 'error'); + } + } catch (error) { + API.toast('删除失败: ' + error.message, 'error'); + } + }, + + async emptyRecycleBin() { + if (this.recycleBin.length === 0) { + API.toast('回收站已经是空的', 'info'); + return; + } + + if (!confirm(`确定要清空回收站吗?这将永久删除 ${this.recycleBin.length} 项数据,此操作不可恢复!`)) { + return; + } + + try { + const response = await fetch('/api/material-purchase/empty-recycle-bin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + + const result = await response.json(); + if (result.ok) { + API.toast(`回收站已清空,删除了 ${result.count} 项`, 'success'); + await this.loadData(); + this.renderRecycleBin(); + } else { + API.toast(result.error || '清空失败', 'error'); + } + } catch (error) { + API.toast('清空失败: ' + error.message, 'error'); + } + }, + + // 导入相关功能 + showImportDialog() { + const dialog = document.getElementById('import-dialog'); + if (dialog) { + // 重置状态 + document.getElementById('import-file-input').value = ''; + document.getElementById('import-status').style.display = 'none'; + document.getElementById('import-confirm-btn').style.display = 'none'; + dialog.style.display = 'flex'; + } + }, + + closeImportDialog() { + const dialog = document.getElementById('import-dialog'); + if (dialog) { + dialog.style.display = 'none'; + } + }, + + async validateImportFile() { + const fileInput = document.getElementById('import-file-input'); + const file = fileInput.files[0]; + + if (!file) { + API.toast('请选择要导入的文件', 'warning'); + return; + } + + const statusEl = document.getElementById('import-status'); + const confirmBtn = document.getElementById('import-confirm-btn'); + + statusEl.style.display = 'block'; + statusEl.style.background = 'rgba(79, 140, 255, 0.1)'; + statusEl.style.color = 'var(--primary)'; + statusEl.textContent = '正在验证文件...'; + confirmBtn.style.display = 'none'; + + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/api/validate/material-purchase-file', { + method: 'POST', + credentials: 'include', + body: formData + }); + + const result = await response.json(); + + if (result.valid) { + statusEl.style.background = 'rgba(34, 197, 94, 0.1)'; + statusEl.style.color = '#22c55e'; + statusEl.textContent = '✓ ' + result.message; + confirmBtn.style.display = 'inline-block'; + } else { + statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; + statusEl.style.color = '#ef4444'; + statusEl.textContent = '✗ ' + result.message; + confirmBtn.style.display = 'none'; + } + } catch (error) { + statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; + statusEl.style.color = '#ef4444'; + statusEl.textContent = '✗ 验证失败: ' + error.message; + confirmBtn.style.display = 'none'; + } + }, + + async confirmImport() { + const fileInput = document.getElementById('import-file-input'); + const file = fileInput.files[0]; + + if (!file) { + API.toast('请选择要导入的文件', 'warning'); + return; + } + + const statusEl = document.getElementById('import-status'); + const confirmBtn = document.getElementById('import-confirm-btn'); + + statusEl.style.display = 'block'; + statusEl.style.background = 'rgba(79, 140, 255, 0.1)'; + statusEl.style.color = 'var(--primary)'; + statusEl.textContent = '正在导入数据...'; + confirmBtn.disabled = true; + + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/api/upload/material-purchase-file', { + method: 'POST', + credentials: 'include', + body: formData + }); + + const result = await response.json(); + + if (result.ok) { + statusEl.style.background = 'rgba(34, 197, 94, 0.1)'; + statusEl.style.color = '#22c55e'; + statusEl.textContent = '✓ ' + result.message; + + API.toast(`成功导入 ${result.count} 条数据`, 'success'); + + // 延迟关闭对话框并刷新数据 + setTimeout(async () => { + this.closeImportDialog(); + await this.loadData(); + }, 1500); + } else { + statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; + statusEl.style.color = '#ef4444'; + statusEl.textContent = '✗ ' + (result.error || '导入失败'); + confirmBtn.disabled = false; + } + } catch (error) { + statusEl.style.background = 'rgba(239, 68, 68, 0.1)'; + statusEl.style.color = '#ef4444'; + statusEl.textContent = '✗ 导入失败: ' + error.message; + confirmBtn.disabled = false; + } + } + }; + + // 页面加载后初始化 + Router.onAfterEach((path) => { + if (path === '/plan-mgmt/material-purchase') { + setTimeout(() => MaterialPurchase.init(), 100); + } + }); +})(); diff --git a/frontend/js/components/work-order.js b/frontend/js/components/work-order.js new file mode 100644 index 0000000..eb73d59 --- /dev/null +++ b/frontend/js/components/work-order.js @@ -0,0 +1,487 @@ +(() => { + async function render() { + return ` +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
工厂/项目名称工单/订单号产品型号订单数量订单生产开始时间订单生产结束时间当前状态备注操作
暂无数据
+
+ + +
+ + + + `; + } + + Router.register('/production-mgmt/work-order', async () => { + const html = await render(); + setTimeout(async () => { + await initPage(); + }, 0); + return html; + }); + + // 监听路由变化,离开工单页面时清理定时器 + let lastPath = ''; + window.addEventListener('hashchange', () => { + const currentPath = location.hash.replace('#', ''); + if (lastPath === '/production-mgmt/work-order' && currentPath !== '/production-mgmt/work-order') { + stopAutoRefresh(); + } + lastPath = currentPath; + }); + + let currentPage = 1; + let totalPages = 1; + let workOrders = []; + let currentUser = null; + let refreshInterval = null; + + async function initPage() { + // 获取当前用户信息 + try { + currentUser = await API.me(); + } catch (error) { + console.error('获取用户信息失败:', error); + } + + // 根据用户角色显示/隐藏添加按钮 + const addBtn = document.getElementById('add-work-order-btn'); + if (addBtn && currentUser?.role !== 'superadmin') { + addBtn.style.display = 'none'; + } + + initEventListeners(); + loadWorkOrders(); + + // 启动自动刷新(每10秒刷新一次) + startAutoRefresh(); + } + + function startAutoRefresh() { + // 清除已存在的定时器 + if (refreshInterval) { + clearInterval(refreshInterval); + } + + // 每10秒自动刷新一次工单列表 + refreshInterval = setInterval(() => { + loadWorkOrders(); + }, 10000); + } + + function stopAutoRefresh() { + if (refreshInterval) { + clearInterval(refreshInterval); + refreshInterval = null; + } + } + + function initEventListeners() { + const searchBtn = document.getElementById('search-btn'); + const resetBtn = document.getElementById('reset-btn'); + const prevBtn = document.getElementById('prev-page'); + const nextBtn = document.getElementById('next-page'); + const addBtn = document.getElementById('add-work-order-btn'); + const modal = document.getElementById('work-order-modal'); + const closeModal = document.getElementById('close-modal'); + const cancelModal = document.getElementById('cancel-modal'); + const saveBtn = document.getElementById('save-work-order'); + + searchBtn?.addEventListener('click', () => { + currentPage = 1; + loadWorkOrders(); + }); + + resetBtn?.addEventListener('click', () => { + document.getElementById('factory-filter').value = ''; + document.getElementById('order-filter').value = ''; + document.getElementById('model-filter').value = ''; + document.getElementById('date-filter').value = ''; + currentPage = 1; + loadWorkOrders(); + }); + + prevBtn?.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + loadWorkOrders(); + } + }); + + nextBtn?.addEventListener('click', () => { + if (currentPage < totalPages) { + currentPage++; + loadWorkOrders(); + } + }); + + // 添加工单按钮 + addBtn?.addEventListener('click', () => { + openModal(); + }); + + // 关闭弹窗 + closeModal?.addEventListener('click', () => { + closeModalWindow(); + }); + + cancelModal?.addEventListener('click', () => { + closeModalWindow(); + }); + + // 点击弹窗外部关闭 + modal?.addEventListener('click', (e) => { + if (e.target === modal) { + closeModalWindow(); + } + }); + + // 保存工单 + saveBtn?.addEventListener('click', () => { + saveWorkOrder(); + }); + } + + async function loadWorkOrders() { + const tbody = document.getElementById('work-order-tbody'); + const pageInfo = document.getElementById('page-info'); + const prevBtn = document.getElementById('prev-page'); + const nextBtn = document.getElementById('next-page'); + + // 获取筛选条件 + const factory = document.getElementById('factory-filter')?.value || ''; + const order = document.getElementById('order-filter')?.value || ''; + const model = document.getElementById('model-filter')?.value || ''; + const date = document.getElementById('date-filter')?.value || ''; + + try { + // 调用后端API获取数据 + const params = new URLSearchParams(); + if (factory) params.append('factory', factory); + if (order) params.append('order', order); + if (date) params.append('date', date); + + const response = await fetch(`/api/work-orders?${params.toString()}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const result = await response.json(); + + if (result.ok) { + workOrders = result.data || []; + totalPages = 1; + } else { + throw new Error(result.error || '加载失败'); + } + + const isSuperAdmin = currentUser?.role === 'superadmin'; + const isAdmin = currentUser?.role === 'admin' || isSuperAdmin; + const colspanCount = isSuperAdmin ? 9 : (isAdmin ? 9 : 8); + + if (workOrders.length === 0) { + tbody.innerHTML = `暂无数据`; + } else { + tbody.innerHTML = workOrders.map(order => { + const statusClass = order.status === 'confirmed' ? 'status-confirmed' : 'status-issued'; + const statusText = order.statusText || '已下发'; + const isConfirmed = order.status === 'confirmed'; + + return ` + + ${order.factory} + ${order.orderNo} + ${order.productModel || '-'} + ${order.orderQty} + ${order.productionStartTime || '-'} + ${order.productionEndTime || '-'} + ${statusText} + ${order.remark || '-'} + ${isSuperAdmin ? ` + + + + + ` : isAdmin ? ` + + ${!isConfirmed ? `` : '已确认'} + + ` : ''} + + `; + }).join(''); + } + + // 根据用户角色显示/隐藏操作列表头 + const thead = document.querySelector('.data-table thead tr'); + const operationTh = thead?.querySelector('th:last-child'); + if (operationTh) { + operationTh.style.display = (isSuperAdmin || isAdmin) ? '' : 'none'; + } + + // 更新分页信息 + pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`; + prevBtn.disabled = currentPage <= 1; + nextBtn.disabled = currentPage >= totalPages; + + } catch (error) { + console.error('加载工单数据失败:', error); + API.toast('加载数据失败,请稍后重试'); + const isSuperAdmin = currentUser?.role === 'superadmin'; + const isAdmin = currentUser?.role === 'admin' || isSuperAdmin; + const colspanCount = isSuperAdmin ? 9 : (isAdmin ? 9 : 8); + tbody.innerHTML = `加载失败`; + } + } + + // 打开弹窗 + function openModal(order = null) { + const modal = document.getElementById('work-order-modal'); + const modalTitle = document.getElementById('modal-title'); + const form = document.getElementById('work-order-form'); + + form.reset(); + + if (order) { + // 编辑模式 + modalTitle.textContent = '编辑工单'; + document.getElementById('form-order-id').value = order.id; + document.getElementById('form-factory').value = order.factory; + document.getElementById('form-order-no').value = order.orderNo; + document.getElementById('form-product-model').value = order.productModel || ''; + document.getElementById('form-order-qty').value = order.orderQty; + document.getElementById('form-start-time').value = order.productionStartTime || ''; + document.getElementById('form-end-time').value = order.productionEndTime || ''; + document.getElementById('form-remark').value = order.remark || ''; + } else { + // 添加模式 + modalTitle.textContent = '添加工单'; + document.getElementById('form-order-id').value = ''; + } + + modal.style.display = 'flex'; + } + + // 关闭弹窗 + function closeModalWindow() { + const modal = document.getElementById('work-order-modal'); + modal.style.display = 'none'; + } + + // 保存工单 + async function saveWorkOrder() { + const orderId = document.getElementById('form-order-id').value; + const factory = document.getElementById('form-factory').value.trim(); + const orderNo = document.getElementById('form-order-no').value.trim(); + const productModel = document.getElementById('form-product-model').value.trim(); + const orderQty = document.getElementById('form-order-qty').value; + const startTime = document.getElementById('form-start-time').value; + const endTime = document.getElementById('form-end-time').value; + const remark = document.getElementById('form-remark').value.trim(); + + // 验证必填项 + if (!factory || !orderNo || !orderQty) { + API.toast('请填写所有必填项'); + return; + } + + const data = { + factory, + orderNo, + productModel, + orderQty: parseInt(orderQty), + productionStartTime: startTime, + productionEndTime: endTime, + remark + }; + + try { + let response; + + if (orderId) { + // 编辑 - 更新现有工单 + response = await fetch(`/api/work-orders/${orderId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + } else { + // 添加 - 新增工单 + response = await fetch('/api/work-orders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + } + + const result = await response.json(); + + if (result.ok) { + API.toast(result.message || (orderId ? '工单更新成功' : '工单添加成功')); + closeModalWindow(); + // 确保数据保存后再刷新列表 + await loadWorkOrders(); + } else { + API.toast(result.error || '保存失败'); + } + } catch (error) { + console.error('保存工单失败:', error); + API.toast('保存失败,请稍后重试'); + } + } + + // 暴露全局函数供按钮调用 + window.editWorkOrderById = function(id) { + const order = workOrders.find(o => o.id === id); + if (order) { + openModal(order); + } + }; + + window.deleteWorkOrder = async function(id) { + if (confirm('确定要删除这条工单吗?')) { + try { + const response = await fetch(`/api/work-orders/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }); + + const result = await response.json(); + + if (result.ok) { + API.toast(result.message || '工单删除成功'); + await loadWorkOrders(); + } else { + API.toast(result.error || '删除失败'); + } + } catch (error) { + console.error('删除工单失败:', error); + API.toast('删除失败,请稍后重试'); + } + } + }; + + window.confirmWorkOrder = async function(id) { + if (confirm('确定要确认这条工单吗?')) { + try { + const response = await fetch(`/api/work-orders/${id}/confirm`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + const result = await response.json(); + + if (result.ok) { + API.toast(result.message || '工单确认成功'); + await loadWorkOrders(); + } else { + API.toast(result.error || '确认失败'); + } + } catch (error) { + console.error('确认工单失败:', error); + API.toast('确认失败,请稍后重试'); + } + } + }; +})(); diff --git a/frontend/login.html b/frontend/login.html new file mode 100644 index 0000000..b1adef5 --- /dev/null +++ b/frontend/login.html @@ -0,0 +1,335 @@ + + + + + +登录 - 韬智生产管理系统 + + + + + + + +
+
+
+ + + + + +
+
+ + + + diff --git a/frontend/sop_files/sop_20251122172515_xlsx b/frontend/sop_files/sop_20251122172515_xlsx new file mode 100644 index 0000000..1d805b7 Binary files /dev/null and b/frontend/sop_files/sop_20251122172515_xlsx differ diff --git a/frontend/sop_files/sop_20251122172848_file.xlsx b/frontend/sop_files/sop_20251122172848_file.xlsx new file mode 100644 index 0000000..1d805b7 Binary files /dev/null and b/frontend/sop_files/sop_20251122172848_file.xlsx differ diff --git a/frontend/sop_files/sop_20251122175115_010286AD2891-SOP.xlsx b/frontend/sop_files/sop_20251122175115_010286AD2891-SOP.xlsx new file mode 100644 index 0000000..42b5230 Binary files /dev/null and b/frontend/sop_files/sop_20251122175115_010286AD2891-SOP.xlsx differ diff --git a/material_purchase_template.csv b/material_purchase_template.csv new file mode 100644 index 0000000..19b284a --- /dev/null +++ b/material_purchase_template.csv @@ -0,0 +1,4 @@ +标题,生产计划明细物料需求清单编号,生产计划编号,产品BOM分析结果,状态,需求状态,物料齐套率,物料编码,物料名称,物料批次号,物料层级,所需物料数,库存现有物料数,欠缺值,物料获取方式,实时库存值,待入库数量,派发数量,入库数量 +物料需求计划示例1,MRL-2024-0001,PLAN-2024-0001,BOM分析-1,pending,urgent,85.5,MAT-00001,电阻10K,BATCH-2024-001,1,1000,855,145,purchase,900,50,100,855 +物料需求计划示例2,MRL-2024-0002,PLAN-2024-0001,BOM分析-1,processing,normal,92.3,MAT-00002,电容100uF,BATCH-2024-002,1,500,461,39,production,480,20,50,461 +物料需求计划示例3,MRL-2024-0003,PLAN-2024-0002,BOM分析-2,completed,low,100,MAT-00003,芯片STM32,BATCH-2024-003,2,200,200,0,transfer,200,0,0,200 diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..aba4aad --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,6 @@ +Flask>=2.3.0 +Werkzeug>=2.3.0 +redis>=4.5.0 +openpyxl>=3.1.0 +reportlab>=4.0.0 +Pillow>=10.0.0 diff --git a/sop_template_example.csv b/sop_template_example.csv new file mode 100644 index 0000000..f909264 --- /dev/null +++ b/sop_template_example.csv @@ -0,0 +1,8 @@ +步骤,操作内容,注意事项,负责人 +1,准备物料,检查物料是否齐全,确认批次号,物料员 +2,设备检查,检查设备运行状态,确认参数设置,设备操作员 +3,首件检验,生产首件后进行质量检验,质检员 +4,批量生产,按照标准流程进行批量生产,生产操作员 +5,过程检验,每小时抽检一次,记录检验结果,质检员 +6,成品检验,完成生产后进行全检,质检员 +7,包装入库,合格品包装并入库,记录批次信息,仓管员 diff --git a/test-theme.html b/test-theme.html deleted file mode 100644 index a31c76b..0000000 --- a/test-theme.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - -主题测试 - - - - -
- - -
- -
-

上传日志测试

-
-
上传日志
-
-[2024-01-01 10:00:00] 开始上传文件...
-[2024-01-01 10:00:01] 验证文件格式...
-[2024-01-01 10:00:02] 处理数据中...
-[2024-01-01 10:00:03] 成功上传 100 条记录
-[2024-01-01 10:00:04] 完成!
-    
-
-
- -
-

日期选择器测试

- -
- -
-

卡片测试

-
-
测试卡片
-

这是一个测试卡片,用于验证主题颜色是否正确。

- -
-
- - - - diff --git a/user.svg b/user.svg new file mode 100644 index 0000000..6834ce2 --- /dev/null +++ b/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/validate_excel.py b/validate_excel.py deleted file mode 100644 index a9b3a02..0000000 --- a/validate_excel.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -""" -验证Excel文件格式是否符合MAC与批次导入要求 -""" -import sys -import pandas as pd -import warnings - -warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl') - -def validate_excel(file_path): - """ - 验证Excel文件格式 - 返回: (is_valid, error_message) - """ - try: - df = pd.read_excel(file_path) - - if df.empty: - return False, "文件为空,没有数据" - - columns = df.columns.tolist() - - # 检查是否有批次号列 - if '批次号' not in columns: - return False, "缺少必需的列:批次号" - - # 检查是否有MAC或SN_MAC列 - has_mac = 'MAC' in columns - has_sn_mac = 'SN_MAC' in columns - - if not has_mac and not has_sn_mac: - return False, "缺少必需的列:MAC 或 SN_MAC" - - # 检查列数(应该只有2列) - if len(columns) != 2: - return False, f"文件应该只包含2列数据,当前有{len(columns)}列:{', '.join(columns)}" - - # 验证通过 - mac_col = 'MAC' if has_mac else 'SN_MAC' - return True, f"文件格式正确,包含列:{mac_col} 和 批次号,共{len(df)}行数据" - - except Exception as e: - return False, f"读取文件失败:{str(e)}" - -if __name__ == '__main__': - if len(sys.argv) < 2: - print("用法: python validate_excel.py ") - sys.exit(1) - - file_path = sys.argv[1] - is_valid, message = validate_excel(file_path) - - if is_valid: - print(f"✓ {message}") - sys.exit(0) - else: - print(f"✗ {message}") - sys.exit(1)