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 `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提示:回收站中的数据将保留30天,过期后将自动永久删除
+
+
+
+
+
+ | 标题 |
+ 物料需求清单编号 |
+ 物料编码 |
+ 物料名称 |
+ 删除时间 |
+ 操作 |
+
+
+
+
+ |
+ 回收站为空
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Excel文件要求:
+ • 必须包含以下列:标题、生产计划明细物料需求清单编号、生产计划编号、状态、需求状态、物料编码、物料名称、所需物料数
+ • 可选列:产品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)