整理目录
This commit is contained in:
parent
61721d7102
commit
c0b19800a9
132
batch_import.py
132
batch_import.py
@ -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}")
|
||||
@ -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}")
|
||||
@ -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
428
frontend/assets/login.css
Normal 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;
|
||||
}
|
||||
}
|
||||
1320
frontend/js/components/material-purchase.js
Normal file
1320
frontend/js/components/material-purchase.js
Normal file
File diff suppressed because it is too large
Load Diff
487
frontend/js/components/work-order.js
Normal file
487
frontend/js/components/work-order.js
Normal 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">×</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
335
frontend/login.html
Normal 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>
|
||||
BIN
frontend/sop_files/sop_20251122172515_xlsx
Normal file
BIN
frontend/sop_files/sop_20251122172515_xlsx
Normal file
Binary file not shown.
BIN
frontend/sop_files/sop_20251122172848_file.xlsx
Normal file
BIN
frontend/sop_files/sop_20251122172848_file.xlsx
Normal file
Binary file not shown.
BIN
frontend/sop_files/sop_20251122175115_010286AD2891-SOP.xlsx
Normal file
BIN
frontend/sop_files/sop_20251122175115_010286AD2891-SOP.xlsx
Normal file
Binary file not shown.
4
material_purchase_template.csv
Normal file
4
material_purchase_template.csv
Normal 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
|
||||
|
6
server/requirements.txt
Normal file
6
server/requirements.txt
Normal 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
8
sop_template_example.csv
Normal file
@ -0,0 +1,8 @@
|
||||
步骤,操作内容,注意事项,负责人
|
||||
1,准备物料,检查物料是否齐全,确认批次号,物料员
|
||||
2,设备检查,检查设备运行状态,确认参数设置,设备操作员
|
||||
3,首件检验,生产首件后进行质量检验,质检员
|
||||
4,批量生产,按照标准流程进行批量生产,生产操作员
|
||||
5,过程检验,每小时抽检一次,记录检验结果,质检员
|
||||
6,成品检验,完成生产后进行全检,质检员
|
||||
7,包装入库,合格品包装并入库,记录批次信息,仓管员
|
||||
|
@ -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
1
user.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
@ -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)
|
||||
Loading…
Reference in New Issue
Block a user