整理目录

This commit is contained in:
zzh 2025-11-24 09:32:37 +08:00
parent 61721d7102
commit c0b19800a9
16 changed files with 2589 additions and 415 deletions

View File

@ -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}")

View File

@ -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}")

View File

@ -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("完成!您可以使用这些模板文件进行测试。")

428
frontend/assets/login.css Normal file
View File

@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,487 @@
(() => {
async function render() {
return `
<div class="page-container">
<div class="page-header">
<h1>生产工单下发中心</h1>
<button id="add-work-order-btn" class="btn btn-primary">+ 添加工单</button>
</div>
<div class="filter-section">
<div class="filter-row">
<div class="filter-item">
<label>工厂/项目名称</label>
<input type="text" id="factory-filter" placeholder="请输入工厂/项目名称" />
</div>
<div class="filter-item">
<label>工单</label>
<input type="text" id="order-filter" placeholder="请输入工单号" />
</div>
<div class="filter-item">
<label>型号</label>
<input type="text" id="model-filter" placeholder="请输入型号" />
</div>
<div class="filter-item">
<label>生产日期</label>
<input type="date" id="date-filter" />
</div>
<div class="filter-actions">
<button id="search-btn" class="btn btn-primary">查询</button>
<button id="reset-btn" class="btn btn-secondary">重置</button>
</div>
</div>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>工厂/项目名称</th>
<th>工单/订单号</th>
<th>产品型号</th>
<th>订单数量</th>
<th>订单生产开始时间</th>
<th>订单生产结束时间</th>
<th>当前状态</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody id="work-order-tbody">
<tr>
<td colspan="9" style="text-align:center;padding:40px;">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button id="prev-page" class="btn btn-secondary" disabled>上一页</button>
<span id="page-info"> 1 / 1 </span>
<button id="next-page" class="btn btn-secondary" disabled>下一页</button>
</div>
</div>
<!-- 添加/编辑工单弹窗 -->
<div id="work-order-modal" class="modal" style="display:none;">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">添加工单</h2>
<button class="modal-close" id="close-modal">&times;</button>
</div>
<div class="modal-body">
<form id="work-order-form">
<div class="form-grid">
<div class="field">
<label>工厂/项目名称 <span class="required">*</span></label>
<input type="text" id="form-factory" class="input" required placeholder="请输入工厂/项目名称" />
</div>
<div class="field">
<label>工单/订单号 <span class="required">*</span></label>
<input type="text" id="form-order-no" class="input" required placeholder="请输入工单/订单号" />
</div>
<div class="field">
<label>产品型号</label>
<input type="text" id="form-product-model" class="input" placeholder="请输入产品型号" />
</div>
<div class="field">
<label>订单数量 <span class="required">*</span></label>
<input type="number" id="form-order-qty" class="input" required placeholder="请输入订单数量" min="1" />
</div>
<div class="field">
<label>订单生产开始时间</label>
<input type="datetime-local" id="form-start-time" class="input" />
</div>
<div class="field">
<label>订单生产结束时间</label>
<input type="datetime-local" id="form-end-time" class="input" />
</div>
<div class="field full-width">
<label>备注</label>
<textarea id="form-remark" class="input" rows="3" placeholder="请输入备注信息"></textarea>
</div>
</div>
<input type="hidden" id="form-order-id" />
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="cancel-modal">取消</button>
<button type="button" class="btn btn-primary" id="save-work-order">保存</button>
</div>
</div>
</div>
`;
}
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 = `<tr><td colspan="${colspanCount}" style="text-align:center;padding:40px;">暂无数据</td></tr>`;
} 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 `
<tr>
<td>${order.factory}</td>
<td>${order.orderNo}</td>
<td>${order.productModel || '-'}</td>
<td>${order.orderQty}</td>
<td>${order.productionStartTime || '-'}</td>
<td>${order.productionEndTime || '-'}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td>${order.remark || '-'}</td>
${isSuperAdmin ? `
<td>
<button class="btn-icon" onclick="editWorkOrderById('${order.id}')" title="编辑"></button>
<button class="btn-icon btn-danger" onclick="deleteWorkOrder('${order.id}')" title="删除">🗑</button>
</td>
` : isAdmin ? `
<td>
${!isConfirmed ? `<button class="btn-icon btn-confirm" onclick="confirmWorkOrder('${order.id}')" title="确认">✓</button>` : '<span style="color:var(--text-2);font-size:12px;">已确认</span>'}
</td>
` : ''}
</tr>
`;
}).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 = `<tr><td colspan="${colspanCount}" style="text-align:center;padding:40px;color:var(--danger);">加载失败</td></tr>`;
}
}
// 打开弹窗
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('确认失败,请稍后重试');
}
}
};
})();

335
frontend/login.html Normal file
View File

@ -0,0 +1,335 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>登录 - 韬智生产管理系统</title>
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./assets/login.css" />
</head>
<body>
<div class="login-container">
<div class="login-background"></div>
<div class="login-card">
<div class="login-header">
<div class="logo-container">
<div class="logo-icon">
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 工厂主体 -->
<path d="M8 52h48v8H8z" fill="url(#grad1)"/>
<!-- 左侧建筑 -->
<path d="M8 28h16v24H8z" fill="url(#grad2)"/>
<!-- 中间建筑 -->
<path d="M24 20h16v32H24z" fill="url(#grad3)"/>
<!-- 右侧建筑 -->
<path d="M40 24h16v28H40z" fill="url(#grad4)"/>
<!-- 烟囱 -->
<rect x="14" y="16" width="4" height="12" rx="1" fill="#60a5fa"/>
<rect x="30" y="10" width="4" height="10" rx="1" fill="#60a5fa"/>
<rect x="46" y="14" width="4" height="10" rx="1" fill="#60a5fa"/>
<!-- 烟雾 -->
<circle cx="16" cy="14" r="2" fill="#93c5fd" opacity="0.6"/>
<circle cx="18" cy="12" r="2.5" fill="#93c5fd" opacity="0.5"/>
<circle cx="32" cy="8" r="2" fill="#93c5fd" opacity="0.6"/>
<circle cx="34" cy="6" r="2.5" fill="#93c5fd" opacity="0.5"/>
<circle cx="48" cy="12" r="2" fill="#93c5fd" opacity="0.6"/>
<circle cx="50" cy="10" r="2.5" fill="#93c5fd" opacity="0.5"/>
<!-- 窗户 -->
<rect x="12" y="34" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="18" y="34" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="12" y="42" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="18" y="42" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="28" y="26" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="34" y="26" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="28" y="34" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="34" y="34" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="28" y="42" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="34" y="42" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="44" y="30" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="50" y="30" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="44" y="38" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<rect x="50" y="38" width="4" height="4" rx="0.5" fill="#dbeafe"/>
<!-- 大门 -->
<rect x="28" y="48" width="8" height="4" rx="0.5" fill="#1e40af"/>
<!-- 渐变定义 -->
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1e3a8a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
</linearGradient>
<linearGradient id="grad2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
</linearGradient>
<linearGradient id="grad3" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2563eb;stop-opacity:1" />
</linearGradient>
<linearGradient id="grad4" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#60a5fa;stop-opacity:1" />
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
</linearGradient>
</defs>
</svg>
</div>
<h1 class="system-title">韬智生产管理系统</h1>
</div>
<p class="system-subtitle">Production Management System</p>
</div>
<div class="login-form">
<div class="form-group">
<label for="username">用户名</label>
<div class="input-wrapper">
<span class="input-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/>
<path d="M6 21c0-3.314 2.686-6 6-6s6 2.686 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
<input
type="text"
id="username"
class="form-input"
placeholder="请输入用户名"
autocomplete="username"
/>
</div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="input-wrapper">
<span class="input-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="11" width="14" height="10" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="16" r="1.5" fill="currentColor"/>
</svg>
</span>
<input
type="password"
id="password"
class="form-input"
placeholder="请输入密码"
autocomplete="current-password"
/>
</div>
</div>
<div class="form-group">
<label for="captcha">验证码</label>
<div class="captcha-wrapper">
<div class="input-wrapper" style="flex: 1;">
<span class="input-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M7 9h2m3 0h2m3 0h2M7 13h3m4 0h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
<input
type="text"
id="captcha"
class="form-input"
placeholder="请输入验证码"
maxlength="4"
autocomplete="off"
/>
</div>
<div class="captcha-image-wrapper" id="captcha-image-wrapper" title="点击刷新验证码">
<img id="captcha-image" class="captcha-image" alt="验证码" />
</div>
</div>
</div>
<div id="error-message" class="error-message" style="display:none;"></div>
<button id="login-btn" class="login-btn">
<span id="login-text">登录</span>
<span id="login-loader" class="btn-loader" style="display:none;">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
</button>
</div>
<div class="login-footer">
<p>© 2025 韬智科技 · 智能制造</p>
</div>
</div>
</div>
<script>
// 检查是否已登录
async function checkAuth() {
try {
const response = await fetch('/api/auth/me', {
credentials: 'include'
});
if (response.ok) {
const user = await response.json();
if (user && user.username) {
window.location.replace('/index.html#/dashboard');
}
}
} catch(e) {
// 未登录,继续显示登录页面
console.log('未登录,显示登录页面');
}
}
checkAuth();
// 登录处理
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const captchaInput = document.getElementById('captcha');
const captchaImage = document.getElementById('captcha-image');
const captchaImageWrapper = document.getElementById('captcha-image-wrapper');
const loginBtn = document.getElementById('login-btn');
const loginText = document.getElementById('login-text');
const loginLoader = document.getElementById('login-loader');
const errorMessage = document.getElementById('error-message');
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
setTimeout(() => {
errorMessage.style.display = 'none';
}, 3000);
}
function setLoading(loading) {
if (loading) {
loginBtn.disabled = true;
loginText.style.display = 'none';
loginLoader.style.display = 'flex';
} else {
loginBtn.disabled = false;
loginText.style.display = 'inline';
loginLoader.style.display = 'none';
}
}
// 加载验证码
async function loadCaptcha() {
try {
const response = await fetch('/api/auth/captcha', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.image) {
captchaImage.src = data.image;
}
}
} catch(e) {
console.error('加载验证码失败:', e);
}
}
// 点击图片刷新验证码
captchaImageWrapper.addEventListener('click', () => {
captchaInput.value = '';
captchaImageWrapper.classList.add('refreshing');
loadCaptcha();
setTimeout(() => {
captchaImageWrapper.classList.remove('refreshing');
}, 300);
});
// 页面加载时获取验证码
loadCaptcha();
async function handleLogin() {
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
const captcha = captchaInput.value.trim();
if (!username) {
showError('请输入用户名');
usernameInput.focus();
return;
}
if (!password) {
showError('请输入密码');
passwordInput.focus();
return;
}
if (!captcha) {
showError('请输入验证码');
captchaInput.focus();
return;
}
if (captcha.length !== 4) {
showError('验证码为4位数字');
captchaInput.focus();
return;
}
setLoading(true);
try {
// 直接使用 fetch避免 API.login 的 overlay 问题
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password, captcha })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '登录失败');
}
if (result.ok) {
// 登录成功,跳转到主页面
// 使用 replace 避免 Safari 缓存问题
window.location.replace('/index.html#/dashboard');
} else {
throw new Error('登录失败');
}
} catch(e) {
showError(e.message || '登录失败');
setLoading(false);
// 刷新验证码
captchaInput.value = '';
loadCaptcha();
}
}
loginBtn.addEventListener('click', handleLogin);
// 回车键登录
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
passwordInput.focus();
}
});
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
captchaInput.focus();
}
});
captchaInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleLogin();
}
});
// 自动聚焦用户名输入框
usernameInput.focus();
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

@ -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
1 标题 生产计划明细物料需求清单编号 生产计划编号 产品BOM分析结果 状态 需求状态 物料齐套率 物料编码 物料名称 物料批次号 物料层级 所需物料数 库存现有物料数 欠缺值 物料获取方式 实时库存值 待入库数量 派发数量 入库数量
2 物料需求计划示例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
3 物料需求计划示例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
4 物料需求计划示例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

6
server/requirements.txt Normal file
View File

@ -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

8
sop_template_example.csv Normal file
View File

@ -0,0 +1,8 @@
步骤,操作内容,注意事项,负责人
1,准备物料,检查物料是否齐全,确认批次号,物料员
2,设备检查,检查设备运行状态,确认参数设置,设备操作员
3,首件检验,生产首件后进行质量检验,质检员
4,批量生产,按照标准流程进行批量生产,生产操作员
5,过程检验,每小时抽检一次,记录检验结果,质检员
6,成品检验,完成生产后进行全检,质检员
7,包装入库,合格品包装并入库,记录批次信息,仓管员
1 步骤 操作内容 注意事项 负责人
2 1 准备物料 检查物料是否齐全,确认批次号 物料员
3 2 设备检查 检查设备运行状态,确认参数设置 设备操作员
4 3 首件检验 生产首件后进行质量检验 质检员
5 4 批量生产 按照标准流程进行批量生产 生产操作员
6 5 过程检验 每小时抽检一次,记录检验结果 质检员
7 6 成品检验 完成生产后进行全检 质检员
8 7 包装入库 合格品包装并入库,记录批次信息 仓管员

View File

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>主题测试</title>
<link rel="stylesheet" href="frontend/assets/styles.css" />
<style>
body { padding: 20px; }
.test-section { margin: 20px 0; padding: 20px; border: 2px solid var(--border); border-radius: 8px; }
.theme-toggle { margin-bottom: 20px; }
</style>
</head>
<body>
<div class="theme-toggle">
<button class="btn" onclick="toggleTheme()">切换主题</button>
<span id="current-theme"></span>
</div>
<div class="test-section">
<h3>上传日志测试</h3>
<div id="upload-log">
<div style="font-weight:600;margin-bottom:8px">上传日志</div>
<pre style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-size:12px;color:var(--text);white-space:pre-wrap">
[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] 完成!
</pre>
</div>
</div>
<div class="test-section">
<h3>日期选择器测试</h3>
<input type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" />
</div>
<div class="test-section">
<h3>卡片测试</h3>
<div class="card">
<div style="font-weight:600;margin-bottom:8px">测试卡片</div>
<p>这是一个测试卡片,用于验证主题颜色是否正确。</p>
<ul class="list">
<li><span>项目1</span><span class="badge">标签</span></li>
<li><span>项目2</span><span class="badge">标签</span></li>
</ul>
</div>
</div>
<script>
function toggleTheme() {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
const newTheme = current === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeDisplay();
}
function updateThemeDisplay() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
document.getElementById('current-theme').textContent = `当前主题: ${current === 'light' ? '浅色' : '深色'}`;
}
// 初始化
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeDisplay();
</script>
</body>
</html>

1
user.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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 <excel文件路径>")
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)