界面优化

This commit is contained in:
zzh 2026-05-14 10:53:41 +08:00
parent 03358b5426
commit bd27a69944
45 changed files with 1377 additions and 2482 deletions

View File

@ -1,13 +0,0 @@
# Redis 配置
# 如果 Redis 在本地运行,使用 127.0.0.1 可以显著提升性能
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=Zzh08165511
# 管理员密码
SUPERADMIN_PASSWORD=Zzh08165511
# 服务器配置
HOST=0.0.0.0
PORT=5000

66
.gitignore vendored Executable file → Normal file
View File

@ -1,64 +1,8 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
node_modules/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Database
*.db
!server/data.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
server.log
# Environment variables
.env
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
coverage/
.DS_Store
Thumbs.db
# Uploaded files
frontend/assets/avatars/*
!frontend/assets/avatars/.gitkeep
# Redis dump
dump.rdb
# Temporary files
*.tmp
*.bak
*.cache
# Documentation
README/
*.log
.env*
!.env.example

2
.vscode/settings.json vendored Executable file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -1,206 +0,0 @@
# 智能生产报表AI分析功能
## 功能概述
智能报表功能利用AI分析生产数据自动生成包含以下内容的生产报表
- 生产总览和关键指标
- 平台产量分布
- 质量问题分析
- AI洞察和改进建议
- 产量预测
## 支持的AI提供商
1. **OpenAI** - GPT-3.5/GPT-4
2. **通义千问** - 阿里云
3. **文心一言** - 百度
4. **本地模型** - Ollama等
## 配置步骤
### 1. 后端配置
#### 1.1 安装依赖
```bash
cd backend
pip install aiohttp fastapi python-dotenv
```
#### 1.2 配置环境变量
```bash
# 复制配置文件
cp .env.example .env
# 编辑配置文件
nano .env
```
#### 1.3 配置示例
**使用OpenAI**
```
AI_PROVIDER=openai
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
```
**使用通义千问:**
```
AI_PROVIDER=qwen
QWEN_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
```
**使用文心一言:**
```
AI_PROVIDER=wenxin
WENXIN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
WENXIN_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
```
**使用本地模型Ollama**
```
AI_PROVIDER=local
LOCAL_AI_URL=http://localhost:11434/api/generate
LOCAL_MODEL=llama2
```
### 2. 启动本地模型(可选)
如果使用本地模型需要先启动Ollama
```bash
# 安装Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# 启动Ollama服务
ollama serve
# 下载模型
ollama pull llama2
# 或下载中文模型
ollama pull qwen:7b
```
### 3. 集成到后端
在主应用中注册AI API路由
```python
# main.py 或 app.py
from api_ai import router as ai_router
app.include_router(ai_router)
```
### 4. 前端使用
前端已经集成AI报表组件位于仪表盘页面。用户可以
- 点击"生成智能报表"按钮
- 查看AI分析结果
- 导出报表为文本文件
- 刷新获取最新分析
## API接口
### 1. 分析生产数据
```
POST /api/ai/analyze
```
响应示例:
```json
{
"summary": {
"totalProduction": 15423,
"goodRate": "98.5%",
"trend": "up",
"insights": [
"本周产量较上周增长12%,主要得益于圆通订单的增加",
"良品率保持在98%以上,质量管控效果显著",
"建议:继续保持当前生产节奏,关注设备维护"
]
},
"platforms": {
"pdd": {
"count": 8934,
"percentage": 57.9,
"trend": "+5.2%"
},
"yt": {
"count": 6489,
"percentage": 42.1,
"trend": "+18.7%"
}
},
"quality": {
"topIssues": [
{"issue": "外观划痕", "count": 23, "percentage": "0.15%"},
{"issue": "功能异常", "count": 12, "percentage": "0.08%"},
{"issue": "包装破损", "count": 8, "percentage": "0.05%"}
]
},
"prediction": {
"tomorrow": 2250,
"weekRange": "15500-16500",
"confidence": "92%"
},
"metadata": {
"generated_at": "2024-01-20T10:30:00",
"data_period": "最近30天",
"total_records": 15423,
"ai_provider": "openai"
}
}
```
### 2. 获取AI配置信息
```
GET /api/ai/config
```
### 3. 测试AI连接
```
POST /api/ai/test
```
### 4. 获取支持的提供商
```
GET /api/ai/providers
```
## 注意事项
1. **API密钥安全**请妥善保管API密钥不要提交到版本控制
2. **成本控制**使用云端AI服务会产生费用建议设置使用限额
3. **响应时间**AI分析可能需要几秒钟请耐心等待
4. **数据隐私**生产数据会发送给AI服务请确保符合隐私政策
## 故障排除
### 1. AI分析失败
- 检查API密钥是否正确
- 确认网络连接正常
- 查看后端日志获取详细错误信息
### 2. 本地模型连接失败
- 确认Ollama服务正在运行
- 检查模型是否已下载
- 验证URL配置是否正确
### 3. 分析结果不准确
- 调整提示词修改ai_service.py中的prompt
- 尝试不同的AI模型
- 增加数据量或改进数据质量
## 扩展功能
1. **自定义分析维度**:可以修改提示词,添加更多分析维度
2. **定时生成报表**:可以设置定时任务,自动生成日报/周报
3. **报表模板**支持多种报表格式PDF、Excel等
4. **历史记录**:保存历史分析结果,支持对比查看
## 技术支持
如有问题,请查看:
1. 后端日志:`/logs/ai_service.log`
2. 浏览器控制台错误信息
3. API响应状态和错误消息

View File

@ -1,45 +0,0 @@
# 🎯 问题修复总结
## 问题描述
计算铨宝采购需求时,期初库存显示的是友辉的数据。例如:
- 铨宝的"6*6*7.5支架 TS-1166VW"期初库存应该是 0但显示 995友辉的库存
- 铨宝的"0402B102K500NT"期初库存应该是 0但显示 1243友辉的库存
## 根本原因
**`/api/purchase-demand/recalculate` 接口**在查询期初库存时,只用了 `material_code`,没有按 `factory` 过滤:
```python
# 错误的查询第7408行
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
```
当友辉和铨宝有相同的物料编码时,这个查询会返回第一个匹配的记录(通常是友辉的),导致铨宝的采购需求使用了友辉的库存数据。
## 修复方案
在查询时添加工厂过滤条件:
```python
# 正确的查询第7409行
factory = item['factory'] or '友辉' # 从采购需求记录中获取工厂字段
c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=? AND factory=?', (material_code, factory))
```
## 已修复的接口
1. ✅ `/api/purchase-demand/calculate` - 计算单个产品采购需求
2. ✅ `/api/purchase-demand/calculate-all` - 计算所有产品采购需求
3. ✅ `/api/purchase-demand/calculate-from-orders` - 从客户订单计算采购需求
4. ✅ `/api/purchase-demand/recalculate` - 重新计算现有采购需求(**这个是你使用的接口**
## 验证步骤
1. 重启服务器:`systemctl restart prod-mgmt`
2. 在前端选择"铨宝"工厂
3. 选择产品(如"AP05商超")并输入数量
4. 点击"重新计算"
5. 检查采购需求列表中的期初库存字段:
- 铨宝的"6*6*7.5支架 TS-1166VW"应该显示 0 ✅
- 铨宝的"0402B102K500NT"应该显示 0 ✅
- 铨宝的"0402B104K160NT"应该显示 30124 ✅
## 其他修复
- ✅ 期初库存导入:支持同一物料在不同工厂独立存在
- ✅ 数据备份恢复:导入/删除前自动备份,可一键恢复

View File

@ -1,43 +0,0 @@
# 🔄 需要重启服务器
## 修复内容
已修复以下问题:
### 1. 期初库存导入
- ✅ 支持同一物料编码在不同工厂独立存在
- ✅ 导入时按 `(material_code, factory)` 组合检查是否存在
### 2. 采购需求计算
- ✅ `calculate_purchase_demand` - 添加工厂参数,按工厂匹配期初库存
- ✅ `calculate_all_purchase_demand` - 添加工厂参数,按工厂匹配期初库存
- ✅ `calculate_purchase_demand_from_orders` - 修复INSERT语句字段顺序factory字段放在最后
### 3. 数据备份恢复
- ✅ 导入Excel前自动备份
- ✅ 批量删除前自动备份
- ✅ 恢复数据前自动备份
- ✅ 前端添加"📦 数据恢复"功能
## 重启后需要做的事
1. **删除错误的采购需求记录**
- 需求单 D20260401105945062 及其他使用旧代码生成的记录
- 这些记录的期初库存字段不正确
2. **重新计算采购需求**
- 使用修复后的代码重新计算
- 验证期初库存字段正确匹配工厂
3. **验证修复效果**
- 铨宝的"6*6*7.5支架 TS-1166VW"期初库存应该是 0
- 友辉的"6*6*7.5支架 TS-1166VW"期初库存应该是 995
## 如何重启
```bash
cd /home/hyx/work/生产管理系统
# 停止当前服务器Ctrl+C 或 kill 进程)
# 然后重新启动
python3 server/app.py
```

View File

@ -1,37 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
customer_orders 表添加 customer_name
"""
import sqlite3
import os
# 数据库路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, 'server', 'data.db')
def add_column():
"""添加 customer_name 列"""
if not os.path.exists(DB_PATH):
print(f"错误: 数据库文件不存在: {DB_PATH}")
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
try:
# 尝试添加列
c.execute('ALTER TABLE customer_orders ADD COLUMN customer_name TEXT')
conn.commit()
print("成功添加 customer_name 列")
except Exception as e:
if 'duplicate column name' in str(e).lower():
print("customer_name 列已存在")
else:
print(f"添加列失败: {e}")
conn.close()
if __name__ == '__main__':
add_column()

Binary file not shown.

View File

@ -1,31 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""检查对账单数据"""
import sqlite3
conn = sqlite3.connect('server/data.db')
c = conn.cursor()
# 统计总数
c.execute('SELECT COUNT(*) FROM reconciliations')
total = c.fetchone()[0]
print(f'对账单总数: {total}')
# 查看最新10条记录
c.execute('''
SELECT id, order_date, contract_no, material_name, spec_model,
quantity, unit, unit_price, total_amount, delivery_date
FROM reconciliations
ORDER BY id DESC
LIMIT 10
''')
print('\n最新10条记录:')
print('-' * 120)
for row in c.fetchall():
print(f"ID: {row[0]}, 下单: {row[1]}, 合同: {row[2]}, 物料: {row[3]}, 规格: {row[4]}")
print(f" 数量: {row[5]} {row[6]}, 单价: {row[7]}, 金额: {row[8]}, 交货: {row[9]}")
print('-' * 120)
conn.close()

View File

@ -1,4 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

569
frontend/assets/login.css.bak Executable file
View File

@ -0,0 +1,569 @@
* {
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-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at 20% 30%,
rgba(138, 43, 226, 0.8) 0%,
rgba(138, 43, 226, 0) 60%
),
radial-gradient(
ellipse at 80% 50%,
rgba(0, 191, 255, 0.7) 0%,
rgba(0, 191, 255, 0) 70%
),
radial-gradient(
ellipse at 50% 80%,
rgba(50, 205, 50, 0.6) 0%,
rgba(50, 205, 50, 0) 65%
),
linear-gradient(135deg, #000000 0%, #0a0520 100%);
background-blend-mode: overlay, screen, hard-light;
overflow: hidden;
animation: aurora-drift 25s infinite alternate ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.login-wrapper::before {
content: "";
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background: repeating-linear-gradient(
45deg,
rgba(255, 255, 255, 0.02) 0px,
rgba(255, 255, 255, 0.02) 1px,
transparent 1px,
transparent 40px
),
repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.03) 0px,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px,
transparent 60px
);
animation: grid-shift 20s linear infinite;
pointer-events: none;
}
.login-wrapper::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: radial-gradient(
circle at center,
transparent 70%,
rgba(10, 5, 32, 0.9) 100%
);
animation: aurora-pulse 8s infinite alternate;
pointer-events: none;
}
@keyframes aurora-drift {
0% {
background-position:
0% 0%,
0% 0%,
0% 0%;
filter: hue-rotate(0deg) brightness(1);
}
50% {
background-position:
-10% -5%,
5% 10%,
0% 15%;
filter: hue-rotate(30deg) brightness(1.2);
}
100% {
background-position:
5% 10%,
-10% -5%,
15% 0%;
filter: hue-rotate(60deg) brightness(1);
}
}
@keyframes grid-shift {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(-50%, -50%);
}
}
@keyframes aurora-pulse {
0% {
opacity: 0.8;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.05);
}
100% {
opacity: 0.8;
transform: scale(1);
}
}
/* 主容器 - 适配极光背景 */
.container {
display: flex;
width: 620px;
height: 520px;
max-width: 95%;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
background-color: rgba(10, 5, 32, 0.25);
border-radius: 20px;
box-shadow: 0px 0px 60px rgba(138, 43, 226, 0.2),
0px 0px 40px rgba(0, 191, 255, 0.1),
inset 0 0 30px rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
animation: slideUp 0.6s ease-out, container-glow 4s ease-in-out infinite alternate;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes container-glow {
0% {
box-shadow: 0px 0px 60px rgba(138, 43, 226, 0.2),
0px 0px 40px rgba(0, 191, 255, 0.1),
inset 0 0 30px rgba(255, 255, 255, 0.05);
}
50% {
box-shadow: 0px 0px 80px rgba(138, 43, 226, 0.3),
0px 0px 60px rgba(0, 191, 255, 0.2),
inset 0 0 40px rgba(255, 255, 255, 0.08);
}
100% {
box-shadow: 0px 0px 60px rgba(50, 205, 50, 0.2),
0px 0px 40px rgba(0, 191, 255, 0.15),
inset 0 0 30px rgba(255, 255, 255, 0.05);
}
}
.container::after {
position: absolute;
content: "";
width: 80%;
height: 80%;
right: -40%;
background: rgb(157, 173, 203);
background: radial-gradient(
circle,
rgba(157, 173, 203, 0.6) 61%,
rgba(99, 122, 159, 0.4) 100%
);
border-radius: 50%;
z-index: -1;
pointer-events: none;
}
/* 左侧表单区域 */
.left {
width: 60%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 40px;
}
/* 表单头部 */
.form-header {
text-align: center;
margin-bottom: 30px;
}
.system-title {
font-size: 22px;
font-weight: 700;
color: #ffffff;
margin: 0 0 6px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.system-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 1px;
text-transform: uppercase;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
/* 表单样式 */
.form {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
position: relative;
}
.form::before {
position: absolute;
content: "";
width: 50%;
height: 50%;
right: -10%;
top: 10%;
z-index: -1;
pointer-events: none;
background: radial-gradient(
circle,
rgba(194, 13, 170, 0.3) 20%,
rgba(26, 186, 235, 0.2) 60%,
rgba(26, 186, 235, 0.1) 100%
);
filter: blur(60px);
border-radius: 50%;
}
/* 输入框块 */
.input-block {
position: relative;
margin-bottom: 8px;
z-index: 1;
}
.input,
button {
background: rgba(255, 255, 255, 0.1);
outline: none;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
padding: 12px 14px;
margin: 8px auto;
width: 100%;
display: block;
color: #ffffff;
font-weight: 500;
font-size: 1em;
font-family: inherit;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.input-block label {
position: absolute;
left: 14px;
top: 37%;
pointer-events: none;
color: rgba(255, 255, 255, 0.7);
font-size: 0.95em;
transition: all 0.4s ease;
}
.input:focus + label,
.input:valid + label {
transform: translateY(-120%) scale(0.9);
color: #ffffff;
font-weight: 600;
text-shadow: 0 1px 3px rgba(138, 43, 226, 0.5);
}
.input:focus {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(138, 43, 226, 0.5);
box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.2);
}
.input {
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2),
2px 2px 4px rgba(138, 43, 226, 0.1);
}
/* 验证码区域 */
.captcha-block {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
}
.captcha-block .captcha-input {
flex: 1;
width: auto;
min-width: 0;
}
.captcha-block label {
left: 14px;
top: 37%;
}
.captcha-block .input:focus + label,
.captcha-block .input:valid + label {
transform: translateY(-120%) scale(0.9);
}
.captcha-image-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 110px;
height: 48px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
overflow: hidden;
flex-shrink: 0;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 0;
backdrop-filter: blur(10px);
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2),
2px 2px 6px rgba(138, 43, 226, 0.1);
}
.captcha-image-wrapper:hover {
border-color: rgba(138, 43, 226, 0.5);
box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.2),
inset 2px 2px 4px rgba(0, 0, 0, 0.1);
transform: scale(1.03);
}
.captcha-image-wrapper:active {
transform: scale(0.98);
}
.captcha-image-wrapper.refreshing {
animation: captcha-refresh 0.4s ease;
}
@keyframes captcha-refresh {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(0.95); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.captcha-image {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
pointer-events: none;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
/* 底部链接 */
.forgot {
display: block;
margin: 5px 0 10px 0;
color: rgba(255, 255, 255, 0.9);
font-size: 0.85em;
}
.forgot a {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
transition: color 0.3s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.forgot a:hover {
color: #ffffff;
}
/* 登录按钮 */
button {
background: linear-gradient(135deg, rgba(138, 43, 226, 0.8) 0%, rgba(0, 191, 255, 0.8) 100%);
color: white;
font-size: 1em;
font-weight: 600;
box-shadow: 0 4px 20px rgba(138, 43, 226, 0.3),
0 2px 10px rgba(0, 191, 255, 0.2);
cursor: pointer;
border: none;
position: relative;
overflow: hidden;
backdrop-filter: blur(10px);
}
button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
button:hover {
background: linear-gradient(135deg, rgba(138, 43, 226, 0.9) 0%, rgba(0, 191, 255, 0.9) 100%);
transform: translateY(-2px);
box-shadow: 0 6px 30px rgba(138, 43, 226, 0.4),
0 4px 20px rgba(0, 191, 255, 0.3);
}
button:hover::before {
left: 100%;
}
button:active {
transform: translateY(0);
}
button: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;
}
}
/* 错误消息 */
.error-message {
background: rgba(254, 242, 242, 0.95);
border: 1px solid #fecaca;
color: #dc2626;
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
margin-bottom: 10px;
animation: shake 0.4s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
/* 右侧插图区域 */
.right {
width: 40%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.img {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.img svg {
max-width: 100%;
max-height: 100%;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
flex-direction: column;
width: 95%;
height: auto;
max-height: 90vh;
}
.left {
width: 100%;
padding: 30px 25px;
}
.right {
display: none;
}
.system-title {
font-size: 20px;
}
.container::after {
display: none;
}
}
@media (max-width: 480px) {
.left {
padding: 25px 20px;
}
.system-title {
font-size: 18px;
}
.input, button {
padding: 10px 12px;
font-size: 0.95em;
}
}

View File

@ -2804,9 +2804,16 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
#app.trackit-layout > #sidebar .topnav{flex:1;min-height:0;overflow:visible;padding-right:2px}
#app.trackit-layout > #sidebar .sidebar-footer{margin-top:auto;display:flex;flex-direction:column;gap:12px}
#app.trackit-layout > #sidebar .sidebar-actions{display:flex;align-items:center;justify-content:flex-end;gap:14px;padding:12px 6px 2px;border-top:1px solid rgba(226,232,240,.85)}
#app.trackit-layout > #sidebar .sidebar-user-top{display:flex;align-items:center;padding:10px 6px 8px;border-bottom:1px solid rgba(226,232,240,.85);margin-bottom:4px;width:100%}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-user-top{border-bottom-color:rgba(255,255,255,.08)}
#app.trackit-layout > #sidebar .sidebar-user-top .user-avatar-btn{display:flex;align-items:center;gap:8px;background:none;border:none;cursor:pointer;padding:4px 6px;border-radius:8px;transition:background 0.2s;width:100%}
#app.trackit-layout > #sidebar .sidebar-user-top .user-avatar-btn:hover{background:rgba(79,140,255,0.1)}
#app.trackit-layout > #sidebar .sidebar-user-top .user-avatar-img{width:32px;height:32px;border-radius:50%;flex-shrink:0}
#app.trackit-layout > #sidebar .sidebar-user-top .user-name-display{color:var(--text);font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#app.trackit-layout > #sidebar .sidebar-user-top .user-dropdown{position:absolute;top:calc(100% + 4px);left:0;bottom:auto;min-width:160px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);z-index:1000}
#app.trackit-layout > #sidebar .sidebar-actions{display:flex;align-items:center;justify-content:flex-start;gap:14px;padding:12px 6px 2px;border-top:1px solid rgba(226,232,240,.85)}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-actions{border-top:1px solid rgba(255,255,255,.08)}
#app.trackit-layout > #sidebar .sidebar-actions .notification-bell-btn{position:relative;background:none;border:none;cursor:pointer;padding:6px;border-radius:10px;display:flex;align-items:center;justify-content:center;transition:all 0.2s ease;color:var(--text-2)}
#app.trackit-layout > #sidebar .sidebar-actions .notification-bell-btn{position:relative;background:none;border:none;cursor:pointer;padding:6px;border-radius:10px;display:flex;align-items:center;justify-content:center;transition:all 0.2s ease;color:var(--text-2);margin-left:auto}
#app.trackit-layout > #sidebar .sidebar-actions .notification-bell-btn:hover{background:rgba(79,140,255,0.1);color:var(--primary)}
#app.trackit-layout > #sidebar .sidebar-actions .notification-bell-btn .notification-badge{position:absolute;top:-2px;right:-2px;background:var(--danger);color:#fff;border-radius:10px;padding:2px 6px;font-size:10px;font-weight:700;min-width:18px;text-align:center;line-height:1.2;box-shadow:0 2px 4px rgba(239,68,68,0.3)}
#app.trackit-layout > #sidebar .sidebar-actions .user-avatar-btn{padding:0;opacity:1}

View File

@ -266,13 +266,6 @@
</svg>
</span>
</label>
<button id="notification-bell-btn" class="notification-bell-btn" title="消息通知">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>
<span id="notification-badge" class="notification-badge" style="display:none;">0</span>
</button>
<div class="user-menu-container">
<button id="user-avatar-btn" class="user-avatar-btn">
<img id="user-avatar-img" src="./assets/user-avatar.svg" alt="用户头像" class="user-avatar-img" />
@ -289,6 +282,13 @@
</div>
</div>
</div>
<button id="notification-bell-btn" class="notification-bell-btn" title="消息通知">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>
<span id="notification-badge" class="notification-badge" style="display:none;">0</span>
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,494 @@
const AIReport = (() => {
// 添加样式
const addStyles = () => {
if (document.getElementById('ai-report-styles')) return;
const style = document.createElement('style');
style.id = 'ai-report-styles';
style.textContent = `
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes aiPulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
}
.thinking-content > div:last-child::after {
content: '';
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
background: #667eea;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
`;
document.head.appendChild(style);
};
return `
<div id="ai-report-card" class="card" style="flex:1;display:flex;flex-direction:column;background:var(--surface);min-height:0;cursor:pointer;position:relative" onclick="AIReport.generateReport()">
<!-- AI图标动画 -->
<div style="position:absolute;top:12px;right:12px;width:24px;height:24px;z-index:1">
<div class="ai-pulse" style="width:100%;height:100%;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:50%;display:flex;align-items:center;justify-content:center;animation:aiPulse 2s infinite">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
</div>
<!-- 标题区域 -->
<div style="font-weight:600;margin-bottom:12px;padding-right:36px">
<span style="display:flex;align-items:center;gap:8px">
<span style="background:linear-gradient(135deg,#667eea,#764ba2);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:700">🤖 智能报表</span>
</span>
</div>
<!-- 报表内容区域 -->
<div id="ai-report-content" style="flex:1;min-height:0;overflow-y:auto;font-size:13px;line-height:1.6;color:var(--text)">
<!-- 默认提示 -->
<div id="ai-report-placeholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center;color:var(--text-2)">
<div style="width:48px;height:48px;background:linear-gradient(135deg,#667eea20,#764ba220);border-radius:12px;display:flex;align-items:center;justify-content:center;margin-bottom:12px">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<div style="font-size:14px;margin-bottom:4px">点击生成智能报表</div>
<div style="font-size:12px">AI将为您分析生产数据并生成洞察</div>
</div>
<!-- 加载状态 -->
<div id="ai-report-loading" style="display:none;height:100%;align-items:center;justify-content:center">
<div style="text-align:center">
<div class="ai-loading" style="width:32px;height:32px;border:3px solid var(--border);border-top-color:#667eea;border-radius:50%;animation:spin 1s linear infinite;margin:0 auto 12px"></div>
<div style="color:var(--text-2)">AI正在分析数据...</div>
</div>
</div>
<!-- 思考过程 -->
<div id="ai-report-thinking" style="display:none;flex:1;overflow:hidden">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text);display:flex;align-items:center;gap:6px">
🤔 AI思考过程
<span style="font-size:11px;color:var(--text-2);font-weight:400">实时分析中</span>
</h4>
<div class="thinking-content" style="background:linear-gradient(135deg,#f8f9ff,#f0f2ff);padding:12px;border-radius:8px;border:1px solid #e0e5ff;height:calc(100% - 30px);overflow-y:auto">
<!-- 思考过程内容将在这里动态添加 -->
</div>
</div>
<!-- 报表内容 -->
<div id="ai-report-result" style="display:none"></div>
</div>
<!-- 底部操作栏 -->
<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<div style="font-size:11px;color:var(--text-2)">
<span id="ai-report-time"></span>
</div>
<div style="display:flex;gap:8px">
<button onclick="event.stopPropagation();AIReport.exportReport()" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text);cursor:pointer;transition:all 0.2s" onmouseover="this.style.background='var(--bg)'" onmouseout="this.style.background=''">
导出
</button>
<button onclick="event.stopPropagation();AIReport.refreshReport()" style="padding:4px 8px;background:linear-gradient(135deg,#667eea,#764ba2);color:white;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:all 0.2s" onmouseover="this.style.transform='translateY(-1px)'" onmouseout="this.style.transform=''">
刷新
</button>
</div>
</div>
</div>
`;
};
// 生成报表
const generateReport = async () => {
const placeholderEl = document.getElementById('ai-report-placeholder');
const loadingEl = document.getElementById('ai-report-loading');
const resultEl = document.getElementById('ai-report-result');
const thinkingEl = document.getElementById('ai-report-thinking');
const timeEl = document.getElementById('ai-report-time');
// 显示加载状态
if(placeholderEl) placeholderEl.style.display = 'none';
if(loadingEl) loadingEl.style.display = 'flex';
if(resultEl) resultEl.style.display = 'none';
// 显示思考过程容器
if(thinkingEl) thinkingEl.style.display = 'block';
// 模拟思考过程的逐步展示
const showThinkingProcess = () => {
const thinkingSteps = [
{ title: '数据概览', content: '正在读取最近30天的生产数据...', delay: 500 },
{ title: '数据概览', content: '发现总产量数据,正在分析良品率...', delay: 800 },
{ title: '规律发现', content: '正在分析平台分布规律...', delay: 1200 },
{ title: '规律发现', content: '发现产量趋势,正在计算增长率...', delay: 1600 },
{ title: '原因推断', content: '正在分析质量问题和根本原因...', delay: 2000 },
{ title: '结论形成', content: '正在生成生产建议和预测...', delay: 2400 },
{ title: '结论形成', content: '整合分析结果,准备输出报告...', delay: 2800 }
];
let currentStep = 0;
const thinkingContent = thinkingEl.querySelector('.thinking-content') || thinkingEl;
const updateThinking = () => {
if (currentStep < thinkingSteps.length) {
const step = thinkingSteps[currentStep];
const stepHtml = `
<div style="margin-bottom:12px;opacity:0;animation:fadeIn 0.5s forwards">
<div style="font-size:12px;font-weight:600;color:#667eea;margin-bottom:4px">
${currentStep + 1}. ${step.title}
</div>
<div style="font-size:12px;line-height:1.6;color:var(--text-2);padding-left:16px">
${step.content}
</div>
</div>
`;
if (currentStep === 0) {
thinkingContent.innerHTML = stepHtml;
} else {
thinkingContent.innerHTML += stepHtml;
}
currentStep++;
setTimeout(updateThinking, step.delay);
}
};
updateThinking();
};
// 开始展示思考过程
showThinkingProcess();
try {
// 调用后端AI分析API
const response = await fetch('/api/ai/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reportData = await response.json();
// 隐藏加载和思考过程
if(loadingEl) loadingEl.style.display = 'none';
if(thinkingEl) thinkingEl.style.display = 'none';
// 渲染报表
renderReport(reportData);
// 更新时间
const now = new Date();
timeEl.textContent = `更新于 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
} catch (error) {
console.error('生成报表失败:', error);
// 如果API调用失败使用模拟数据作为后备
try {
console.log('使用模拟数据作为后备...');
const reportData = await analyzeProductionData();
// 隐藏加载和思考过程
if(loadingEl) loadingEl.style.display = 'none';
if(thinkingEl) thinkingEl.style.display = 'none';
renderReport(reportData);
const now = new Date();
timeEl.textContent = `更新于 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} (模拟)`;
} catch (fallbackError) {
console.error('模拟数据也失败:', fallbackError);
if(loadingEl) loadingEl.style.display = 'none';
if(thinkingEl) thinkingEl.style.display = 'none';
if(resultEl) {
resultEl.style.display = 'block';
resultEl.innerHTML = `
<div style="text-align:center;padding:20px;color:var(--text-2)">
<div style="font-size:48px;margin-bottom:16px">😞</div>
<div style="font-size:14px;margin-bottom:8px">生成失败</div>
<div style="font-size:12px">请检查AI服务配置或稍后重试</div>
<button onclick="AIReport.generateReport()" style="margin-top:12px;padding:6px 12px;background:var(--primary);color:white;border:none;border-radius:6px;cursor:pointer">重试</button>
</div>
`;
}
}
}
};
// 模拟AI分析数据
const analyzeProductionData = async () => {
// 这里应该调用实际的AI API
// 现在返回模拟数据
return {
summary: {
totalProduction: 15423,
goodRate: '98.5%',
trend: 'up',
insights: [
'本周产量较上周增长12%,主要得益于圆通订单的增加',
'良品率保持在98%以上,质量管控效果显著',
'建议:继续保持当前生产节奏,关注设备维护'
]
},
platforms: {
pdd: { count: 8934, percentage: 57.9, trend: '+5.2%' },
yt: { count: 6489, percentage: 42.1, trend: '+18.7%' }
},
quality: {
topIssues: [
{ issue: '外观划痕', count: 23, percentage: '0.15%' },
{ issue: '功能异常', count: 12, percentage: '0.08%' },
{ issue: '包装破损', count: 8, percentage: '0.05%' }
]
},
prediction: {
tomorrow: 2250,
weekRange: '15500-16500',
confidence: '92%'
}
};
};
// 渲染报表内容
const renderReport = (data) => {
const loadingEl = document.getElementById('ai-report-loading');
const resultEl = document.getElementById('ai-report-result');
if(loadingEl) loadingEl.style.display = 'none';
// 格式化思考过程
const formatThinking = (thinking) => {
if (!thinking) return '';
// 按步骤分割
const steps = thinking.split(/第[一二三四五]步[:]/).filter(s => s.trim());
const stepTitles = ['数据概览', '规律发现', '原因推断', '结论形成'];
let html = '';
steps.forEach((step, index) => {
if (step.trim()) {
html += `
<div style="margin-bottom:12px">
<div style="font-size:12px;font-weight:600;color:#667eea;margin-bottom:4px">
${index + 1}. ${stepTitles[index] || `步骤${index + 1}`}
</div>
<div style="font-size:12px;line-height:1.6;color:var(--text-2);padding-left:16px">
${step.trim()}
</div>
</div>
`;
}
});
return html;
};
const html = `
<div style="padding:4px 0">
<!-- AI思考过程 -->
${data.thinking ? `
<div style="margin-bottom:16px">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text);display:flex;align-items:center;gap:6px">
🤔 AI思考过程
<span style="font-size:11px;color:var(--text-2);font-weight:400">了解AI如何分析数据</span>
</h4>
<div style="background:linear-gradient(135deg,#f8f9ff,#f0f2ff);padding:12px;border-radius:8px;border:1px solid #e0e5ff">
${formatThinking(data.thinking)}
</div>
</div>
` : ''}
<!-- 总览 -->
<div style="margin-bottom:16px">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text)">📊 生产总览</h4>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">
<div style="background:var(--bg);padding:8px;border-radius:8px">
<div style="font-size:11px;color:var(--text-2)">总产量</div>
<div style="font-size:16px;font-weight:700;color:var(--text)">${data.summary.totalProduction.toLocaleString()}</div>
<div style="font-size:11px;color:#10B981">${data.summary.trend === 'up' ? '↑' : '↓'} vs 上周</div>
</div>
<div style="background:var(--bg);padding:8px;border-radius:8px">
<div style="font-size:11px;color:var(--text-2)">良品率</div>
<div style="font-size:16px;font-weight:700;color:var(--text)">${data.summary.goodRate}</div>
<div style="font-size:11px;color:var(--text-2)">稳定</div>
</div>
</div>
</div>
<!-- 平台分布 -->
<div style="margin-bottom:16px">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text)">📈 平台分布</h4>
<div style="display:flex;flex-direction:column;gap:8px">
<div style="display:flex;align-items:center;gap:8px">
<img src="assets/pdd.svg" style="width:16px;height:16px" />
<div style="flex:1">
<div style="display:flex;justify-content:space-between;margin-bottom:2px">
<span style="font-size:12px">拼多多</span>
<span style="font-size:12px;font-weight:600">${data.platforms.pdd.count.toLocaleString()} (${data.platforms.pdd.percentage}%)</span>
</div>
<div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden">
<div style="width:${data.platforms.pdd.percentage}%;height:100%;background:#3B82F6;transition:width 0.3s"></div>
</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:8px">
<img src="assets/yt.svg" style="width:16px;height:16px" />
<div style="flex:1">
<div style="display:flex;justify-content:space-between;margin-bottom:2px">
<span style="font-size:12px">圆通</span>
<span style="font-size:12px;font-weight:600">${data.platforms.yt.count.toLocaleString()} (${data.platforms.yt.percentage}%)</span>
</div>
<div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden">
<div style="width:${data.platforms.yt.percentage}%;height:100%;background:#10B981;transition:width 0.3s"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 质量分析 -->
<div style="margin-bottom:16px">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text)">🔍 质量分析</h4>
<div style="font-size:12px;color:var(--text-2);margin-bottom:8px">主要不良项</div>
${data.quality.topIssues.map(issue => `
<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px">
<span>${issue.issue}</span>
<span style="color:#EF4444">${issue.count} (${issue.percentage})</span>
</div>
`).join('') || '<div style="font-size:12px;color:var(--text-2)">暂无不良记录</div>'}
</div>
<!-- AI洞察 -->
<div style="margin-bottom:16px">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text)">💡 AI洞察</h4>
<ul style="margin:0;padding-left:16px;font-size:12px;line-height:1.6;color:var(--text)">
${data.summary.insights.map(insight => `<li style="margin-bottom:4px">${insight}</li>`).join('')}
</ul>
</div>
<!-- 预测 -->
<div>
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text)">🎯 产量预测</h4>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px;font-size:12px">
<div style="background:var(--bg);padding:8px;border-radius:8px;text-align:center">
<div style="color:var(--text-2)">明日预测</div>
<div style="font-size:16px;font-weight:700;color:var(--text)">${data.prediction.tomorrow.toLocaleString()}</div>
</div>
<div style="background:var(--bg);padding:8px;border-radius:8px;text-align:center">
<div style="color:var(--text-2)">本周范围</div>
<div style="font-size:14px;font-weight:600;color:var(--text)">${data.prediction.weekRange}</div>
</div>
</div>
<div style="font-size:11px;color:var(--text-2);margin-top:4px">置信度: ${data.prediction.confidence}</div>
</div>
</div>
`;
if(resultEl) {
resultEl.innerHTML = html;
resultEl.style.display = 'block';
}
};
// 导出报表
const exportReport = () => {
const content = document.getElementById('ai-report-result');
if (!content || content.style.display === 'none') {
alert('请先生成报表');
return;
}
// 创建导出内容
const text = content.innerText;
const blob = new Blob([`智能生产报表\n\n生成时间: ${new Date().toLocaleString()}\n\n${text}`], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `智能报表_${new Date().toISOString().slice(0, 10)}.txt`;
a.click();
URL.revokeObjectURL(url);
};
// 刷新报表
const refreshReport = () => {
generateReport();
};
// 添加CSS动画
const addStyles = () => {
if (document.getElementById('ai-report-styles')) return;
const style = document.createElement('style');
style.id = 'ai-report-styles';
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.thinking-content > div:last-child {
position: relative;
}
.thinking-content > div:last-child::after {
content: '';
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
background: #667eea;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes aiPulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.ai-pulse {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
`;
document.head.appendChild(style);
};
// ...
// 初始化
const init = () => {
addStyles();
};
// 暴露方法
return {
init,
generateReport,
exportReport,
refreshReport,
generateAICard
};
})();
// 初始化
AIReport.init();

View File

@ -224,6 +224,7 @@ const Upload = (() => {
<div class="field">
<label>批量导入(Excel)</label>
<input id="mac-file" type="file" class="input" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" />
<div id="mac-file-status" style="margin-top:6px;font-size:12px;color:var(--text-secondary);display:none;"></div>
</div>
</div>
@ -963,27 +964,44 @@ const Upload = (() => {
// 文件选择后立即验证
addListener(fileEl, 'change', async ()=>{
const file = fileEl.files[0];
if(!file) return;
const statusEl = document.getElementById('mac-file-status');
if(!file) {
if(statusEl) statusEl.style.display = 'none';
return;
}
// 立即显示文件名,避免用户以为文件消失
if(statusEl) {
statusEl.style.display = 'block';
statusEl.style.color = 'var(--text-secondary)';
statusEl.textContent = `已选择: ${file.name}(验证中...`;
}
try{
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/validate/mac-file', {
method: 'POST',
credentials: 'include',
body: formData
});
if(!res.ok){
if(statusEl) { statusEl.style.color = 'var(--warning, #f59e0b)'; statusEl.textContent = `已选择: ${file.name}(验证接口异常,可直接上传)`; }
return;
}
const result = await res.json();
if(!result.valid){
if(statusEl) { statusEl.style.color = '#ef4444'; statusEl.textContent = `格式错误: ${result.message || '文件格式不正确'}`; }
API.toast(result.message || '文件格式不正确');
fileEl.value = '';
return;
}
if(statusEl) { statusEl.style.color = '#10b981'; statusEl.textContent = `${file.name}${result.message || '验证通过'}`; }
API.toast(result.message || '文件验证通过');
}catch(e){
API.toast('文件验证失败');
fileEl.value = '';
if(statusEl) { statusEl.style.color = 'var(--warning, #f59e0b)'; statusEl.textContent = `已选择: ${file.name}(验证异常,可直接上传)`; }
}
});

268
frontend/login.html.bak Executable file
View File

@ -0,0 +1,268 @@
<!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?v=20251231-1425" />
</head>
<body>
<div class="login-wrapper">
<div class="container">
<div class="left">
<div class="form-header">
<h1 class="system-title">韬智生产管理系统</h1>
<p class="system-subtitle">Production Management System</p>
</div>
<form class="form" onsubmit="return false;">
<div class="input-block">
<input class="input" type="text" id="username" required autocomplete="username">
<label for="username">用户名</label>
</div>
<div class="input-block">
<input class="input" type="password" id="password" required autocomplete="current-password">
<label for="password">密码</label>
</div>
<div class="input-block captcha-block">
<input class="input captcha-input" type="text" id="captcha" required maxlength="4" autocomplete="off">
<label for="captcha">验证码</label>
<div class="captcha-image-wrapper" id="captcha-image-wrapper" title="点击刷新验证码">
<img id="captcha-image" class="captcha-image" alt="验证码" />
</div>
</div>
<div id="error-message" class="error-message" style="display:none;"></div>
<div class="input-block">
<span class="forgot"><a href="#">© 2025 韬智科技</a></span>
<button type="button" id="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>
</form>
</div>
<div class="right">
<div class="img">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 731.67004 550.61784" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,334.13393c0,.66003,.53003,1.19,1.19006,1.19H730.48004c.65997,0,1.19-.52997,1.19-1.19,0-.65997-.53003-1.19-1.19-1.19H1.19006c-.66003,0-1.19006,.53003-1.19006,1.19Z" fill="#3f3d56"></path>
<polygon points="466.98463 81.60598 470.81118 130.55703 526.26809 107.39339 494.98463 57.60598 466.98463 81.60598" fill="#a0616a"></polygon>
<circle cx="465.32321" cy="55.18079" r="41.33858" fill="#a0616a"></circle>
<polygon points="387.98463 440.60598 394.98463 503.39339 345.98463 496.60598 361.98463 438.60598 387.98463 440.60598" fill="#a0616a"></polygon>
<polygon points="578.98463 449.60598 585.98463 512.39339 536.98463 505.60598 552.98463 447.60598 578.98463 449.60598" fill="#a0616a"></polygon>
<path d="M462.48463,260.10598c-.66897,0-54.14584,2.68515-89.47714,4.46286-16.72275,.84141-29.45202,15.31527-28.15459,32.00884l12.63173,162.5283,36,1,.87795-131,71.12205,4-3-73Z" fill="#2f2e41"></path>
<path d="M619.48463,259.10598s9,69,2,76c-7,7-226.5-5.5-226.5-5.5,0,0,48.15354-69.53704,56.82677-71.51852,8.67323-1.98148,146.67323-8.98148,146.67323-8.98148l21,10Z" fill="#2f2e41"></path>
<path id="uuid-91047c5b-47d7-4179-8a16-40bd6d529b28-203" d="M335.12666,172.23337c-8.35907-11.69074-9.10267-25.48009-1.66174-30.79863,7.44093-5.31854,20.24665-.15219,28.60713,11.54383,3.40375,4.62627,5.65012,10.00041,6.55111,15.67279l34.79215,49.9814-19.8001,13.70807-35.7745-48.83421c-5.07753-2.68845-9.43721-6.55406-12.71405-11.27326Z" fill="#a0616a"></path>
<path d="M464.98463,112.60598l51-21,96,148s-67,15-90,18c-23,3-49-9-49-9l-8-136Z" fill="#5e7eb6"></path>
<path d="M526.98463,137.60598l-18.5-57.70866,24,18.20866s68,45,68,64c0,19,21,77,21,77,0,0,23.5,19.5,15.5,37.5-8,18,10.5,15.5,12.5,28.5,2,13-28.5,30.5-28.5,30.5,0,0-7.5-73.5-31.5-73.5-24,0-62.5-124.5-62.5-124.5Z" fill="#3f3d56"></path>
<path d="M468.56831,111.13035l-25.08368,9.97563s4,70,8,76c4,6,18,38,18,38v10.42913s-28,8.57087-27,13.57087c1,5,66,19,66,19,0,0-13-40-21-53-8-13-18.91632-113.97563-18.91632-113.97563Z" fill="#3f3d56"></path>
<path d="M452.48463,121.10598s-29-4-34,30c-5,34-1.82283,38.5-1.82283,38.5l-8.17717,19.5-27-30-26,17s47,76,66,74c19-2,47-57,47-57l-16-92Z" fill="#3f3d56"></path>
<path d="M597.32321,270.14478l-14.83858,209.96121-38.5-1.5s-8.5-198.5-8.5-201.5c0-3,4-20,29-21,25-1,32.83858,14.03879,32.83858,14.03879Z" fill="#2f2e41"></path>
<path d="M541.48463,484.10598s20-6,23-2c3,4,20,6,20,6l5,49s-14,10-16,12-55,4-56-8c-1-12,14-27,14-27l10-30Z" fill="#2f2e41"></path>
<path d="M394.48463,470.10598s6-5,8,9c2,14,9,37-1,40-10,3-110,4-110-5v-9l9-7,18.00394-2.869s34.99606-32.131,38.99606-32.131c4,0,17,13,17,13l20-6Z" fill="#2f2e41"></path>
<path d="M505.98463,77.60598s-20-24-28-22-3,5-3,5l-20-22s-16-6-31,13c0,0-9-16,0-25,9-9,12-8,14-13,2-5,16-9,16-9,0,0-.80315-7.19685,3.59843-3.59843s15.3937,3.59843,15.3937,3.59843c0,0,.06299-4,4.53543,0,4.47244,4,9.47244,2,9.47244,2,0,0,0,6.92126,3.5,6.96063,3.5,.03937,9.5-4.96063,10.5-.96063,1,4,8,6,9,18,1,12-4,47-4,47Z" fill="#2f2e41"></path>
<g>
<path d="M342.99463,178.84874l-114.2362,78.82694c-3.94205,2.72015-9.36214,1.72624-12.08229-2.21581l-32.16176-46.60891c-2.72015-3.94205-1.7259-9.36208,2.21615-12.08223l114.2362-78.82694c3.94205-2.72015,9.36214-1.72624,12.08229,2.21581l32.16176,46.60891c2.72015,3.94205,1.7259,9.36208-2.21615,12.08223Z" fill="#fff"></path>
<path d="M312.83914,120.30274l32.16148,46.6085c2.64627,3.83499,1.68408,9.08121-2.15091,11.72749l-56.06388,38.68602c-14.78562-4.04015-28.2774-13.11486-37.66263-26.71596-6.14766-8.9092-9.85314-18.77211-11.26649-28.80885l63.25494-43.6481c3.83499-2.64627,9.08121-1.68408,11.72749,2.15091Z" fill="#e6e6e6"></path>
<path d="M223.84012,260.20913c-3.0791,0-6.10938-1.46094-7.9873-4.18066l-32.16211-46.60938c-1.4668-2.12695-2.01758-4.7002-1.5498-7.24805,.4668-2.54785,1.89551-4.75879,4.02246-6.22559l114.23535-78.82715c4.39746-3.03223,10.44043-1.92285,13.47363,2.4707l32.16211,46.60938c1.4668,2.12695,2.01758,4.7002,1.5498,7.24805-.4668,2.54688-1.89551,4.75879-4.02148,6.22559l-114.23633,78.82715c-1.67578,1.15527-3.59082,1.70996-5.48633,1.70996Zm82.04785-142.80176c-1.50391,0-3.02344,.44043-4.35254,1.35742l-114.23633,78.82715c-1.6875,1.16309-2.82031,2.91797-3.19141,4.94043-.37109,2.02148,.06543,4.06445,1.22949,5.75l32.16211,46.60938c2.40625,3.48633,7.20215,4.36816,10.69043,1.96094l114.2373-78.82715c1.68652-1.16309,2.81934-2.91797,3.19043-4.94043,.37109-2.02148-.06543-4.06445-1.22949-5.75l-32.16211-46.60938c-1.48926-2.1582-3.89453-3.31836-6.33789-3.31836Z" fill="#3f3d56"></path>
<path d="M224.6666,236.93718c-2.89521,1.9978-3.6253,5.97848-1.6275,8.87369,1.9978,2.89521,5.97848,3.6253,8.87369,1.6275l11.76134-8.11573c2.89521-1.9978,3.6253-5.97848,1.6275-8.87369-1.9978-2.89521-5.97848-3.6253-8.87369-1.6275l-11.76134,8.11573Z" fill="#5e7eb6"></path>
<path d="M232.63862,171.91114c-4.56802,3.15209-5.71978,9.43286-2.56769,14.00088,3.15209,4.56802,9.43252,5.71972,14.00054,2.56763l18.29546-12.6245c4.56802-3.15209,5.72007-9.43245,2.56797-14.00047-3.15209-4.56802-9.4328-5.72013-14.00082-2.56804l-18.29546,12.6245Z" fill="#5e7eb6"></path>
</g>
<g>
<path d="M340.25926,185.80874H201.4659c-4.78947,0-8.68608-3.89636-8.68608-8.68583v-56.62834c0-4.78947,3.89661-8.68583,8.68608-8.68583h138.79336c4.78947,0,8.68608,3.89636,8.68608,8.68583v56.62834c0,4.78947-3.89661,8.68583-8.68608,8.68583Z" fill="#fff"></path>
<path d="M348.69017,120.49482v56.62784c0,4.65939-3.77152,8.43091-8.43091,8.43091h-68.11583c-9.87497-11.72273-15.82567-26.8544-15.82567-43.37931,0-10.82439,2.55172-21.04674,7.08876-30.11034h76.85275c4.65939,0,8.43091,3.77152,8.43091,8.43091Z" fill="#e6e6e6"></path>
<path d="M340.25907,186.80874H201.4661c-5.34082,0-9.68652-4.34473-9.68652-9.68555v-56.62891c0-5.34082,4.3457-9.68555,9.68652-9.68555h138.79297c5.34082,0,9.68652,4.34473,9.68652,9.68555v56.62891c0,5.34082-4.3457,9.68555-9.68652,9.68555ZM201.4661,112.80874c-4.23828,0-7.68652,3.44727-7.68652,7.68555v56.62891c0,4.23828,3.44824,7.68555,7.68652,7.68555h138.79297c4.23828,0,7.68652-3.44727,7.68652-7.68555v-56.62891c0-4.23828-3.44824-7.68555-7.68652-7.68555H201.4661Z" fill="#3f3d56"></path>
<path d="M209.87637,166.41564c-3.51759,0-6.37931,2.86172-6.37931,6.37931s2.86172,6.37931,6.37931,6.37931h14.28966c3.51759,0,6.37931-2.86172,6.37931-6.37931s-2.86172-6.37931-6.37931-6.37931h-14.28966Z" fill="#5e7eb6"></path>
<path d="M253.36907,117.42253c-5.55,0-10.06511,4.51536-10.06511,10.06536s4.51511,10.06486,10.06511,10.06486h22.22841c5.55,0,10.06511-4.51486,10.06511-10.06486s-4.51511-10.06536-10.06511-10.06536h-22.22841Z" fill="#5e7eb6"></path>
</g>
<g>
<path d="M456.25926,381.80874h-138.79336c-4.78947,0-8.68608-3.89636-8.68608-8.68583v-56.62834c0-4.78947,3.89661-8.68583,8.68608-8.68583h138.79336c4.78947,0,8.68608,3.89636,8.68608,8.68583v56.62834c0,4.78947-3.89661,8.68583-8.68608,8.68583Z" fill="#fff"></path>
<path d="M464.69017,316.49482v56.62784c0,4.65939-3.77152,8.43091-8.43091,8.43091h-68.11583c-9.87497-11.72273-15.82567-26.8544-15.82567-43.37931,0-10.82439,2.55172-21.04674,7.08876-30.11034h76.85275c4.65939,0,8.43091,3.77152,8.43091,8.43091Z" fill="#e6e6e6"></path>
<path d="M456.25907,382.80874h-138.79297c-5.34082,0-9.68652-4.34473-9.68652-9.68555v-56.62891c0-5.34082,4.3457-9.68555,9.68652-9.68555h138.79297c5.34082,0,9.68652,4.34473,9.68652,9.68555v56.62891c0,5.34082-4.3457,9.68555-9.68652,9.68555Zm-138.79297-74c-4.23828,0-7.68652,3.44727-7.68652,7.68555v56.62891c0,4.23828,3.44824,7.68555,7.68652,7.68555h138.79297c4.23828,0,7.68652-3.44727,7.68652-7.68555v-56.62891c0-4.23828-3.44824-7.68555-7.68652-7.68555h-138.79297Z" fill="#3f3d56"></path>
<path d="M325.87637,362.41564c-3.51759,0-6.37931,2.86172-6.37931,6.37931s2.86172,6.37931,6.37931,6.37931h14.28966c3.51759,0,6.37931-2.86172,6.37931-6.37931s-2.86172-6.37931-6.37931-6.37931h-14.28966Z" fill="#5e7eb6"></path>
<path d="M369.36907,313.42253c-5.55,0-10.06511,4.51536-10.06511,10.06536s4.51511,10.06486,10.06511,10.06486h22.22841c5.55,0,10.06511-4.51486,10.06511-10.06486s-4.51511-10.06536-10.06511-10.06536h-22.22841Z" fill="#5e7eb6"></path>
</g>
<path id="uuid-c026fd96-7d81-4b34-bb39-0646c0e08e96-204" d="M465.67391,331.01678c-12.74718,6.63753-26.5046,5.44058-30.72743-2.67249-4.22283-8.11308,2.6878-20.06802,15.44041-26.70621,5.05777-2.72156,10.69376-4.19231,16.43644-4.28916l54.36547-27.44139,10.79681,21.52636-53.36733,28.57487c-3.37375,4.65048-7.81238,8.42516-12.94437,11.00803Z" fill="#a0616a"></path>
<path d="M527.48463,97.10598s56-3,68,27c12,30,22,128,22,128l-122,66.37402-21-32.37402,82-64-29-125Z" fill="#3f3d56"></path>
</svg>
</div>
</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>

View File

@ -1,144 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从Excel导入对账单数据"""
import pandas as pd
import sqlite3
import os
from datetime import datetime, timezone, timedelta
# 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), 'server', 'data.db')
EXCEL_FILE = '25年11月份对账单-易泰勒.xlsx'
def get_beijing_time():
"""获取北京时间UTC+8的ISO格式字符串"""
beijing_tz = timezone(timedelta(hours=8))
return datetime.now(beijing_tz).isoformat()
def import_from_excel():
"""从Excel导入对账单数据"""
# 读取Excel文件
df = pd.read_excel(EXCEL_FILE)
# 打印前20行查看结构
print("Excel文件结构:")
print("=" * 80)
for i in range(min(20, len(df))):
print(f"{i}行: {df.iloc[i].tolist()}")
print("=" * 80)
# 查找表头行(包含"序号"的行)
header_row = None
for i in range(len(df)):
row_values = df.iloc[i].tolist()
if any(str(val).strip() == '序号' for val in row_values if pd.notna(val)):
header_row = i
print(f"\n找到表头行: 第{i}")
print(f"表头内容: {row_values}")
break
if header_row is None:
print("❌ 未找到表头行(包含'序号'的行)")
return
# 重新读取,跳过前面的行,使用找到的行作为表头
df = pd.read_excel(EXCEL_FILE, skiprows=header_row)
# 设置第一行为列名
df.columns = df.iloc[0]
df = df[1:] # 删除第一行(已经作为列名)
df = df.reset_index(drop=True)
print(f"\n数据形状: {df.shape}")
print(f"列名: {df.columns.tolist()}")
print(f"\n前5行数据:")
print(df.head())
# 连接数据库
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = get_beijing_time()
imported_count = 0
# 遍历数据行
for idx, row in df.iterrows():
# 跳过空行或无效行
if pd.isna(row.get('序号')):
continue
try:
# 提取数据
# 处理日期格式
def format_date(date_val):
if pd.isna(date_val):
return ''
if isinstance(date_val, datetime):
return date_val.strftime('%Y/%m/%d')
date_str = str(date_val).strip()
# 如果已经是 YYYY/MM/DD 格式,保持不变
if '/' in date_str:
return date_str
# 如果是 YYYY-MM-DD 格式,转换为 YYYY/MM/DD
if '-' in date_str:
return date_str.split()[0].replace('-', '/')
return date_str
order_date = format_date(row.get('下单时间'))
contract_no = str(row.get('合同编号', '')).strip() if pd.notna(row.get('合同编号')) else ''
material_name = str(row.get('物料名称', '')).strip() if pd.notna(row.get('物料名称')) else ''
spec_model = str(row.get('规格型号', '')).strip() if pd.notna(row.get('规格型号')) else ''
transport_no = str(row.get('运输单号', '')).strip() if pd.notna(row.get('运输单号')) else ''
quantity = int(row.get('数量', 0)) if pd.notna(row.get('数量')) else 0
unit = str(row.get('单位', 'pcs')).strip() if pd.notna(row.get('单位')) else 'pcs'
unit_price = float(row.get('含税单价', 0)) if pd.notna(row.get('含税单价')) else 0.0
total_amount = float(row.get('含税金额', 0)) if pd.notna(row.get('含税金额')) else 0.0
delivery_date = format_date(row.get('交货日期'))
shipment_date = format_date(row.get('出货日期'))
# 验证必填字段
if not all([order_date, contract_no, material_name, spec_model, quantity, unit, unit_price]):
print(f"⚠️ 跳过第{idx+1}行: 缺少必填字段")
continue
# 插入数据库
c.execute('''
INSERT INTO reconciliations(
order_date, contract_no, material_name, spec_model, transport_no,
quantity, unit, unit_price, total_amount, delivery_date, shipment_date,
created_by, created_at, updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
''', (
order_date,
contract_no,
material_name,
spec_model,
transport_no,
quantity,
unit,
unit_price,
total_amount,
delivery_date,
shipment_date,
'admin',
now,
now
))
imported_count += 1
print(f"✅ 导入第{idx+1}行: {contract_no} - {material_name}")
except Exception as e:
print(f"❌ 导入第{idx+1}行失败: {e}")
continue
conn.commit()
conn.close()
print(f"\n{'='*80}")
print(f"✅ 成功导入 {imported_count} 条对账单数据")
print(f"{'='*80}")
if __name__ == '__main__':
import_from_excel()

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
初始化客户订单数据
根据图片中的数据填充客户订单表
"""
import sqlite3
import os
from datetime import datetime, timezone, timedelta
# 数据库路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, 'server', 'data.db')
def get_beijing_time():
"""获取北京时间UTC+8的ISO格式字符串"""
beijing_tz = timezone(timedelta(hours=8))
return datetime.now(beijing_tz).isoformat()
# 从图片中提取的订单数据 - 客户:易泰勒
orders_data = [
# 2025/4/28 - CGDD001695
{'order_date': '2025-04-28', 'order_no': 'CGDD001695', 'customer_name': '易泰勒', 'material': 'ETAP05\n基站-5.0\nETAP05', 'quantity': 950, 'unit_price': 315.19},
{'order_date': '2025-04-28', 'order_no': 'CGDD001695', 'customer_name': '易泰勒', 'material': 'WD1MK0SMD0551\n蓝牙模块\nPCBCOMPONENT_1_-_DUPLICATE', 'quantity': 950, 'unit_price': 1.3},
{'order_date': '2025-04-28', 'order_no': 'CGDD001695', 'customer_name': '易泰勒', 'material': 'WA0000000040\nPCBA\nCH6121-MODULE-V14', 'quantity': 4750, 'unit_price': 8.05},
# 2025/9/4 - CGDD002429
{'order_date': '2025-09-04', 'order_no': 'CGDD002429', 'customer_name': '易泰勒', 'material': 'ETAP05\n基站-5.0\nETAP05', 'quantity': 1500, 'unit_price': 315.19},
{'order_date': '2025-09-04', 'order_no': 'CGDD002429', 'customer_name': '易泰勒', 'material': 'WD1MK0SMD0551\n蓝牙模块\nPCBCOMPONENT_1_-_DUPLICATE', 'quantity': 1500, 'unit_price': 1.3},
{'order_date': '2025-09-04', 'order_no': 'CGDD002429', 'customer_name': '易泰勒', 'material': 'WA0000000040\nPCBA\nCH6121-MODULE-V14', 'quantity': 7500, 'unit_price': 8.05},
{'order_date': '2025-09-04', 'order_no': 'CGDD002429', 'customer_name': '易泰勒', 'material': 'AP-ET010\n基站-5.0\nETAP05', 'quantity': 500, 'unit_price': 315.19},
{'order_date': '2025-09-04', 'order_no': 'CGDD002429', 'customer_name': '易泰勒', 'material': 'WD1MK0SMD0551\n蓝牙模块\nPCBCOMPONENT_1_-_DUPLICATE', 'quantity': 500, 'unit_price': 1.3},
{'order_date': '2025-09-04', 'order_no': 'CGDD002429', 'customer_name': '易泰勒', 'material': 'WA0000000040\nPCBA\nCH6121-MODULE-V14', 'quantity': 2500, 'unit_price': 8.05},
# 2025/10/23 - CGDD002878
{'order_date': '2025-10-23', 'order_no': 'CGDD002878', 'customer_name': '易泰勒', 'material': 'AP-DZ006\n智能灯条基站\nETAP05-D1', 'quantity': 4000, 'unit_price': 239.2},
{'order_date': '2025-10-23', 'order_no': 'CGDD002878', 'customer_name': '易泰勒', 'material': 'WD1MK0SMD0551\n蓝牙模块\nPCBCOMPONENT_1_-_DUPLICATE', 'quantity': 12000, 'unit_price': 1.1},
# 2025/11/13 - CGDD003037
{'order_date': '2025-11-13', 'order_no': 'CGDD003037', 'customer_name': '易泰勒', 'material': 'AP-DZ009\n智能灯条基站\nETAP05-D1', 'quantity': 500, 'unit_price': 229.61},
{'order_date': '2025-11-13', 'order_no': 'CGDD003037', 'customer_name': '易泰勒', 'material': 'WD1MK0SMD0551\n蓝牙模块\nPCBCOMPONENT_1_-_DUPLICAT', 'quantity': 1500, 'unit_price': 1.1},
{'order_date': '2025-11-13', 'order_no': 'CGDD003037', 'customer_name': '易泰勒', 'material': 'AP-DZ006\n智能灯条基站\nETAP05-D1', 'quantity': 4000, 'unit_price': 239.2},
{'order_date': '2025-11-13', 'order_no': 'CGDD003037', 'customer_name': '易泰勒', 'material': 'WD1MK0SMD0551\n蓝牙模块\nPCBCOMPONENT_1_-_DUPLICATE', 'quantity': 12000, 'unit_price': 1.1},
]
def init_orders():
"""初始化客户订单数据"""
if not os.path.exists(DB_PATH):
print(f"错误: 数据库文件不存在: {DB_PATH}")
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# 检查表是否存在
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='customer_orders'")
if not c.fetchone():
print("错误: customer_orders 表不存在,请先运行服务器以创建表")
conn.close()
return
# 清空现有数据(可选)
c.execute('DELETE FROM customer_orders')
print("已清空现有订单数据")
# 插入新数据
now = get_beijing_time()
inserted_count = 0
for order in orders_data:
try:
c.execute('''INSERT INTO customer_orders(
order_date, order_no, customer_name, material, quantity, unit_price,
created_by, created_at, updated_at
) VALUES(?,?,?,?,?,?,?,?,?)''', (
order['order_date'],
order['order_no'],
order['customer_name'],
order['material'],
order['quantity'],
order['unit_price'],
'admin', # 创建者
now,
now
))
inserted_count += 1
except Exception as e:
print(f"插入订单失败: {order['order_no']} - {e}")
conn.commit()
conn.close()
print(f"成功插入 {inserted_count} 条订单数据")
if __name__ == '__main__':
init_orders()

View File

@ -1,124 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""初始化对账单示例数据"""
import sqlite3
import os
from datetime import datetime, timezone, timedelta
# 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), 'server', 'data.db')
def get_beijing_time():
"""获取北京时间UTC+8的ISO格式字符串"""
beijing_tz = timezone(timedelta(hours=8))
return datetime.now(beijing_tz).isoformat()
def init_reconciliations():
"""初始化对账单示例数据"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# 示例数据(根据图片中的数据)
sample_data = [
{
'order_date': '2025/10/31',
'contract_no': 'CGDD002876',
'material_name': '扩产-9988 红黑线',
'spec_model': 'PCXK0P0NSNT_1_2.54*1C14TE',
'transport_no': '快递上门',
'quantity': 45,
'unit': 'pcs',
'unit_price': 239.2,
'total_amount': 10764,
'delivery_date': '2025/11/3',
'shipment_date': '2025/11/3'
},
{
'order_date': '2025/9/20',
'contract_no': 'CGDD004562',
'material_name': '扩产-9988 红黑线',
'spec_model': 'M1H0EM0N511 PCXK0P0NSNT_1_2.54*1C14TE',
'transport_no': '快递上门',
'quantity': 355,
'unit': 'pcs',
'unit_price': 1.1,
'total_amount': 390.5,
'delivery_date': '2025/11/3',
'shipment_date': '2025/11/3'
},
{
'order_date': '2025/9/20',
'contract_no': 'CGDD004562',
'material_name': '扩产-9988 红黑线',
'spec_model': 'ETAP05-01',
'transport_no': '快递上门',
'quantity': 2,
'unit': 'pcs',
'unit_price': 245.46,
'total_amount': 490.92,
'delivery_date': '2025/11/3',
'shipment_date': '2025/11/3'
},
{
'order_date': '2025/9/20',
'contract_no': 'CGDD004562',
'material_name': 'M1H0EM0N511 红黑线',
'spec_model': 'PCXK0P0NSNT_1_2.54*1C14TE',
'transport_no': '快递上门',
'quantity': 6,
'unit': 'pcs',
'unit_price': 1.1,
'total_amount': 6.6,
'delivery_date': '2025/11/3',
'shipment_date': '2025/11/3'
},
{
'order_date': '2025/10/11',
'contract_no': 'CGDD002717',
'material_name': '扩产-9988 红黑线',
'spec_model': 'ETAP05-01',
'transport_no': '快递上门',
'quantity': 500,
'unit': 'pcs',
'unit_price': 228.45,
'total_amount': 114225,
'delivery_date': '2025/11/3',
'shipment_date': '2025/11/3'
}
]
now = get_beijing_time()
for data in sample_data:
c.execute('''
INSERT INTO reconciliations(
order_date, contract_no, material_name, spec_model, transport_no,
quantity, unit, unit_price, total_amount, delivery_date, shipment_date,
created_by, created_at, updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
''', (
data['order_date'],
data['contract_no'],
data['material_name'],
data['spec_model'],
data['transport_no'],
data['quantity'],
data['unit'],
data['unit_price'],
data['total_amount'],
data['delivery_date'],
data['shipment_date'],
'admin',
now,
now
))
conn.commit()
count = len(sample_data)
conn.close()
print(f'✅ 成功初始化 {count} 条对账单示例数据')
if __name__ == '__main__':
init_reconciliations()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3728,8 +3728,8 @@ def upload_mac_file():
f.save(temp_path)
# 调用batch_import.py脚本
script_path = '/home/hyx/work/生产管理系统/test_py/batch_import.py'
python_path = '/home/hyx/work/.venv/bin/python'
script_path = '/opt/生产管理系统/test_py/batch_import.py'
python_path = '/usr/bin/python3'
try:
result = subprocess.run(
[python_path, script_path, upload_type],

BIN
server/app.pyc Executable file

Binary file not shown.

Binary file not shown.

View File

@ -1,145 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>美团基站测试 - API调试</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.card { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 8px; }
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
.online { background: #d4edda; color: #155724; }
.offline { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>美团基站测试 - API调试</h1>
<div class="card">
<h2>服务器状态</h2>
<button onclick="checkServerStatus()">检查服务器状态</button>
<div id="server-status" class="status">点击按钮检查</div>
<h3>服务器日志:</h3>
<pre id="server-logs" style="max-height: 400px; overflow-y: auto;"></pre>
</div>
<div class="card">
<h2>基站列表</h2>
<button onclick="loadStations()">加载基站列表</button>
<pre id="stations-list">点击按钮加载</pre>
</div>
<div class="card">
<h2>发送命令</h2>
<input type="text" id="station-id" placeholder="基站ID (如: BS003)" style="width: 200px; padding: 5px;">
<input type="text" id="command" placeholder="命令 (如: ping 8.8.8.8)" style="width: 300px; padding: 5px;">
<button onclick="sendCommand()">发送命令</button>
<pre id="command-result" style="margin-top: 10px;"></pre>
</div>
<script>
async function checkServerStatus() {
const statusEl = document.getElementById('server-status');
const logsEl = document.getElementById('server-logs');
statusEl.textContent = '检查中...';
try {
const response = await fetch('/api/meituan/server-status');
const data = await response.json();
if (data.success) {
statusEl.className = 'status online';
statusEl.textContent = `服务器状态: ${data.status === 'online' ? '在线' : '离线'} (基站数: ${data.station_count})`;
if (data.logs && data.logs.length > 0) {
logsEl.textContent = data.logs.join('\n');
} else {
logsEl.textContent = '暂无日志';
}
} else {
statusEl.className = 'status offline';
statusEl.textContent = `错误: ${data.error}`;
}
} catch (error) {
statusEl.className = 'status offline';
statusEl.textContent = `连接失败: ${error.message}`;
console.error('错误:', error);
}
}
async function loadStations() {
const listEl = document.getElementById('stations-list');
listEl.textContent = '加载中...';
try {
const response = await fetch('/api/meituan/stations');
const data = await response.json();
if (data.success) {
if (data.stations && data.stations.length > 0) {
listEl.textContent = JSON.stringify(data.stations, null, 2);
} else {
listEl.textContent = '暂无基站连接';
}
} else {
listEl.textContent = `错误: ${data.error}`;
}
} catch (error) {
listEl.textContent = `加载失败: ${error.message}`;
console.error('错误:', error);
}
}
async function sendCommand() {
const stationId = document.getElementById('station-id').value;
const command = document.getElementById('command').value;
const resultEl = document.getElementById('command-result');
if (!stationId || !command) {
alert('请输入基站ID和命令');
return;
}
resultEl.textContent = '发送中...';
try {
const response = await fetch('/api/meituan/send-command', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
station_id: stationId,
command: command
})
});
const data = await response.json();
if (data.success) {
resultEl.textContent = `成功: ${data.message}`;
} else {
resultEl.textContent = `失败: ${data.error}`;
}
} catch (error) {
resultEl.textContent = `发送失败: ${error.message}`;
console.error('错误:', error);
}
}
// 页面加载时自动检查状态
window.onload = function() {
console.log('页面加载完成,开始检查服务器状态...');
checkServerStatus();
// 每5秒刷新一次状态
setInterval(() => {
console.log('定时刷新服务器状态...');
checkServerStatus();
}, 5000);
};
</script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试XLSX库</title>
</head>
<body>
<h1>XLSX库测试</h1>
<input type="file" id="file-input" accept=".xlsx,.xls">
<div id="result"></div>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script>
document.getElementById('file-input').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
console.log('XLSX typeof:', typeof XLSX);
console.log('XLSX version:', XLSX.version);
const reader = new FileReader();
reader.onload = function(e) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
document.getElementById('result').innerHTML = `
<h2>解析结果:</h2>
<pre>${JSON.stringify(jsonData, null, 2)}</pre>
`;
};
reader.readAsArrayBuffer(file);
});
</script>
</body>
</html>

View File

@ -1,60 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试修复含税金额功能
"""
import requests
import json
def test_fix_total_amount():
"""测试修复含税金额API"""
# API地址
url = "http://localhost:5000/api/reconciliations/fix-total-amount"
# 模拟登录需要先获取session
login_url = "http://localhost:5000/api/login"
try:
# 创建会话
session = requests.Session()
# 登录
login_data = {
"username": "admin",
"password": "admin"
}
print("正在登录...")
login_response = session.post(login_url, json=login_data)
if login_response.status_code == 200:
print("登录成功!")
# 调用修复含税金额API
print("\n正在调用修复含税金额API...")
response = session.post(url)
if response.status_code == 200:
result = response.json()
print(f"\n修复结果:")
print(f"- 状态: {'成功' if result.get('ok') else '失败'}")
print(f"- 修复记录数: {result.get('fixed_count', 0)}")
print(f"- 消息: {result.get('message', '')}")
else:
print(f"API调用失败状态码: {response.status_code}")
print(f"错误信息: {response.text}")
else:
print(f"登录失败,状态码: {login_response.status_code}")
print(f"错误信息: {login_response.text}")
except requests.exceptions.ConnectionError:
print("\n错误:无法连接到服务器")
print("请确保生产管理系统正在运行systemctl start prod-mgmt")
except Exception as e:
print(f"\n发生错误: {str(e)}")
if __name__ == '__main__':
print("=== 测试修复含税金额功能 ===\n")
test_fix_total_amount()

View File

@ -1,232 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
委外管理系统测试脚本
用于验证委外工单发料入库等功能是否正常工作
"""
import sys
import os
# 添加server目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server'))
from app import init_db, get_db
def test_database_tables():
"""测试数据库表是否正确创建"""
print("=" * 60)
print("测试1: 检查数据库表结构")
print("=" * 60)
# 初始化数据库
init_db()
conn = get_db()
c = conn.cursor()
# 检查委外工单表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='outsourcing_orders'")
if c.fetchone():
print("✓ 委外工单表 (outsourcing_orders) 创建成功")
else:
print("✗ 委外工单表创建失败")
return False
# 检查委外发料表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='outsourcing_material_issue'")
if c.fetchone():
print("✓ 委外发料表 (outsourcing_material_issue) 创建成功")
else:
print("✗ 委外发料表创建失败")
return False
# 检查委外在制库存表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='outsourcing_wip_stock'")
if c.fetchone():
print("✓ 委外在制库存表 (outsourcing_wip_stock) 创建成功")
else:
print("✗ 委外在制库存表创建失败")
return False
# 检查成品入库表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='finished_goods_receipt'")
if c.fetchone():
print("✓ 成品入库表 (finished_goods_receipt) 创建成功")
else:
print("✗ 成品入库表创建失败")
return False
conn.close()
print("\n所有数据库表创建成功!\n")
return True
def test_table_structure():
"""测试表结构是否正确"""
print("=" * 60)
print("测试2: 检查表结构字段")
print("=" * 60)
conn = get_db()
c = conn.cursor()
# 检查委外工单表字段
c.execute("PRAGMA table_info(outsourcing_orders)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'order_no', 'customer_order_no', 'product_code',
'product_name', 'production_qty', 'outsourcing_factory',
'delivery_date', 'status', 'created_by', 'created_at', 'updated_at']
print("\n委外工单表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
# 检查委外发料表字段
c.execute("PRAGMA table_info(outsourcing_material_issue)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'issue_no', 'outsourcing_order_no', 'material_code',
'material_name', 'issue_qty', 'unit', 'issue_date',
'created_by', 'created_at', 'updated_at']
print("\n委外发料表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
# 检查成品入库表字段
c.execute("PRAGMA table_info(finished_goods_receipt)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'receipt_no', 'outsourcing_order_no', 'product_code',
'product_name', 'receipt_qty', 'receipt_date',
'created_by', 'created_at', 'updated_at']
print("\n成品入库表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
# 检查委外在制库存表字段
c.execute("PRAGMA table_info(outsourcing_wip_stock)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'outsourcing_order_no', 'material_code',
'material_name', 'wip_qty', 'unit', 'updated_at']
print("\n委外在制库存表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
conn.close()
print("\n表结构检查完成!\n")
return True
def create_test_data():
"""创建测试数据"""
print("=" * 60)
print("测试3: 创建测试数据")
print("=" * 60)
conn = get_db()
c = conn.cursor()
try:
# 创建测试BOM数据
print("\n创建测试BOM数据...")
test_bom = [
('AP05', 'AP05物流版', 'R001', '电阻-10K', 1, 'pcs', 100, '供应商A', None, 'system', '2026-03-13', '2026-03-13'),
('AP05', 'AP05物流版', 'C001', '电容-100uF', 2, 'pcs', 50, '供应商B', None, 'system', '2026-03-13', '2026-03-13'),
('AP05', 'AP05物流版', 'IC001', '芯片-主控', 1, 'pcs', 1, '供应商C', None, 'system', '2026-03-13', '2026-03-13'),
]
for bom in test_bom:
c.execute('''INSERT OR IGNORE INTO bom (
product_code, product_name, material_code, material_name,
unit_qty, unit, min_package, supplier, remark,
created_by, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)''', bom)
print("✓ BOM测试数据创建成功")
# 创建测试期初库存数据
print("\n创建测试期初库存数据...")
test_stock = [
('R001', '电阻-10K', 500, 'pcs', 100, '供应商A', None, 'system', '2026-03-13', '2026-03-13'),
('C001', '电容-100uF', 1000, 'pcs', 50, '供应商B', None, 'system', '2026-03-13', '2026-03-13'),
('IC001', '芯片-主控', 200, 'pcs', 1, '供应商C', None, 'system', '2026-03-13', '2026-03-13'),
]
for stock in test_stock:
c.execute('''INSERT OR IGNORE INTO initial_stock (
material_code, material_name, stock_qty, unit, min_package,
supplier, remark, created_by, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?)''', stock)
print("✓ 期初库存测试数据创建成功")
# 创建测试客户订单数据
print("\n创建测试客户订单数据...")
c.execute('''INSERT OR IGNORE INTO customer_orders (
order_date, order_no, customer_name, material, quantity, unit_price,
created_by, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?)''',
('2026-03-13', 'CO20260313001', '测试客户', 'AP05物流版', 100, 1000.00,
'system', '2026-03-13', '2026-03-13'))
print("✓ 客户订单测试数据创建成功")
conn.commit()
print("\n所有测试数据创建成功!\n")
except Exception as e:
print(f"\n✗ 创建测试数据失败: {str(e)}\n")
conn.rollback()
return False
finally:
conn.close()
return True
def main():
"""主测试函数"""
print("\n")
print("" + "=" * 58 + "")
print("" + " " * 15 + "委外管理系统测试脚本" + " " * 15 + "")
print("" + "=" * 58 + "")
print("\n")
# 运行测试
success = True
if not test_database_tables():
success = False
if not test_table_structure():
success = False
if not create_test_data():
success = False
# 输出测试结果
print("=" * 60)
if success:
print("✓ 所有测试通过!委外管理系统已准备就绪。")
print("\n下一步操作:")
print("1. 启动服务器: cd server && python app.py")
print("2. 访问系统: http://localhost:5000")
print("3. 进入\"委外管理\"菜单开始使用")
else:
print("✗ 部分测试失败,请检查错误信息。")
print("=" * 60)
print("\n")
if __name__ == '__main__':
main()

View File

@ -1,148 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试对账单单价匹配功能 - 详细版本
"""
import sqlite3
import sys
import os
def test_price_matching():
"""测试价格匹配逻辑"""
# 连接数据库
conn = sqlite3.connect('server/data.db')
c = conn.cursor()
print("=== 对账单单价匹配测试 ===\n")
# 获取所有客户订单
print("1. 获取所有客户订单数据...")
c.execute('SELECT order_no, material, unit_price FROM customer_orders')
customer_orders = c.fetchall()
# 构建订单单价字典
order_prices = {}
for order in customer_orders:
order_no = order[0]
if order_no not in order_prices:
order_prices[order_no] = {}
# 处理物料名称(支持换行符分割的多个物料)
materials = str(order[1]).split('\n')
for material in materials:
material = material.strip()
if material:
order_prices[order_no][material] = order[2]
print(f" 共找到 {len(order_prices)} 个不同的合同号")
# 显示每个合同的物料列表
print("\n2. 客户订单中的合同号及物料:")
for order_no, materials in sorted(order_prices.items()):
print(f"\n 合同号: {order_no}")
for material, price in sorted(materials.items()):
print(f" - 物料: {material[:50]}... , 单价: {price}")
# 获取所有对账单
print("\n\n3. 检查对账单匹配情况...")
c.execute('SELECT id, contract_no, material_name, unit_price FROM reconciliations ORDER BY contract_no')
reconciliations = c.fetchall()
# 统计匹配情况
matched = 0
not_matched = []
for recon in reconciliations:
recon_id, contract_no, material_name, current_price = recon
new_price = None
match_type = ""
# 精确匹配合同号
if contract_no in order_prices:
# 尝试精确匹配物料名
if material_name in order_prices[contract_no]:
new_price = order_prices[contract_no][material_name]
match_type = "精确匹配"
else:
# 尝试部分匹配(物料名包含关系)
for order_material, price in order_prices[contract_no].items():
if material_name in order_material or order_material in material_name:
new_price = price
match_type = f"部分匹配(订单物料: {order_material[:30]}...)"
break
# 如果精确匹配没找到,尝试模糊匹配合同号
if new_price is None:
for order_no, materials in order_prices.items():
if contract_no in order_no or order_no in contract_no:
if material_name in materials:
new_price = materials[material_name]
match_type = f"模糊匹配合同号({order_no})"
break
else:
# 尝试部分匹配物料名
for order_material, price in materials.items():
if material_name in order_material or order_material in material_name:
new_price = price
match_type = f"模糊匹配(订单: {order_no}, 物料: {order_material[:30]}...)"
break
if new_price is not None:
break
if new_price is not None:
matched += 1
print(f" ✓ ID={recon_id}, 合同={contract_no}, 匹配类型: {match_type}")
else:
not_matched.append((recon_id, contract_no, material_name))
print(f"\n\n4. 匹配结果统计:")
print(f" 总对账单记录数: {len(reconciliations)}")
print(f" 成功匹配: {matched}")
print(f" 未匹配: {len(not_matched)}")
if not_matched:
print(f"\n5. 未匹配的记录前10条")
for recon_id, contract_no, material_name in not_matched[:10]:
print(f" ✗ ID={recon_id}, 合同号={contract_no}, 物料={material_name[:50]}...")
# 检查是否有相似的合同号
similar_orders = [o for o in order_prices.keys() if contract_no[:10] in o or o[:10] in contract_no]
if similar_orders:
print(f" → 相似合同号: {', '.join(similar_orders)}")
# 测试特定匹配案例
print(f"\n6. 测试特定匹配案例:")
test_cases = [
("CGDD002878", "AP-DZ006灯条基站"),
("CGDD002878", "WD1MK0SMD0551"),
("CGDD001850", "AP-DZ009灯条基站"),
]
for contract_no, material_name in test_cases:
print(f"\n 测试: 合同号={contract_no}, 物料={material_name}")
if contract_no in order_prices:
print(f" 找到合同号,包含 {len(order_prices[contract_no])} 个物料")
for mat, price in order_prices[contract_no].items():
if material_name in mat or mat in material_name:
print(f" ✓ 匹配成功: {mat[:50]}... -> {price}")
break
else:
print(f" ✗ 未找到匹配的物料")
else:
print(f" ✗ 未找到合同号")
# 查找相似合同号
similar = [o for o in order_prices.keys() if contract_no in o or o in contract_no]
if similar:
print(f" → 相似合同号: {similar}")
conn.close()
print("\n=== 优化建议 ===")
print("1. 确保对账单的合同号与客户订单的订单号完全一致")
print("2. 物料名称应该标准化,避免使用简写或别名")
print("3. 可以在上传发货单时自动匹配客户订单单价")
print("4. 对于无法匹配的记录,可以手动设置默认单价")
if __name__ == '__main__':
test_price_matching()

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""
测试采购需求状态更新为"已下单"后期初库存的更新功能
"""
import sqlite3
import sys
import os
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
DB_PATH = 'server/data.db'
def test_purchase_demand_ordered_update():
"""测试采购需求状态更新为已下单后期初库存自动更新"""
# 连接数据库
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
try:
# 1. 创建测试物料
material_code = 'TEST001'
material_name = '测试物料'
# 插入期初库存记录
c.execute('''INSERT OR REPLACE INTO initial_stock
(material_code, material_name, stock_qty, created_at, updated_at)
VALUES (?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))''',
(material_code, material_name, 100))
# 2. 创建采购需求记录
c.execute('''INSERT INTO purchase_demand
(demand_no, material_code, material_name, order_qty, bom_unit_qty,
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))''',
('TEST001', material_code, material_name, 10, 5, 50, 100, 0, 10, 50, 'pending'))
demand_id = c.lastrowid
# 3. 更新状态为已下单
c.execute('''UPDATE purchase_demand
SET status = 'ordered', updated_at = datetime('now', 'localtime')
WHERE id = ?''', (demand_id,))
# 4. 模拟计算新的期初库存:实际采购 - 总需求
actual_purchase_qty = 50
total_demand = 50
expected_stock = actual_purchase_qty - total_demand
# 更新期初库存
c.execute('''INSERT OR REPLACE INTO initial_stock
(material_code, material_name, stock_qty, updated_at)
VALUES (?, ?, ?, datetime('now', 'localtime'))''',
(material_code, material_name, expected_stock))
conn.commit()
# 5. 验证结果
c.execute('SELECT stock_qty FROM initial_stock WHERE material_code = ?', (material_code,))
result = c.fetchone()
if result:
actual_stock = result[0]
print(f"✅ 测试通过!")
print(f" - 物料编码: {material_code}")
print(f" - 实际采购数量: {actual_purchase_qty}")
print(f" - 总需求: {total_demand}")
print(f" - 期望期初库存: {expected_stock}")
print(f" - 实际期初库存: {actual_stock}")
if actual_stock == expected_stock:
print(f" - 期初库存更新正确!")
else:
print(f" - ❌ 期初库存更新错误!")
else:
print("❌ 测试失败:未找到期初库存记录")
# 6. 清理测试数据
c.execute('DELETE FROM purchase_demand WHERE id = ?', (demand_id,))
c.execute('DELETE FROM initial_stock WHERE material_code = ?', (material_code,))
conn.commit()
except Exception as e:
print(f"❌ 测试出错: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == '__main__':
print("开始测试采购需求状态更新功能...")
print("=" * 50)
test_purchase_demand_ordered_update()
print("=" * 50)
print("测试完成!")

View File

@ -1,222 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
登录问题诊断脚本
"""
import sqlite3
import os
DB_PATH = 'server/data.db'
def check_database():
"""检查数据库和用户"""
print("=" * 60)
print("🔍 检查数据库...")
print("=" * 60)
if not os.path.exists(DB_PATH):
print("❌ 数据库文件不存在:", DB_PATH)
return False
print("✅ 数据库文件存在")
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# 检查用户表
users = c.execute('SELECT username, role FROM users').fetchall()
if not users:
print("❌ 没有找到任何用户")
return False
print(f"\n✅ 找到 {len(users)} 个用户:")
print("-" * 60)
for username, role in users:
print(f" 👤 用户名: {username:15s} | 角色: {role}")
conn.close()
return True
except Exception as e:
print(f"❌ 数据库错误: {e}")
return False
def check_server():
"""检查服务器状态"""
print("\n" + "=" * 60)
print("🔍 检查服务器...")
print("=" * 60)
import subprocess
# 检查进程
try:
result = subprocess.run(
['ps', 'aux'],
capture_output=True,
text=True
)
if 'python' in result.stdout and 'app.py' in result.stdout:
print("✅ 服务器正在运行")
# 提取进程信息
for line in result.stdout.split('\n'):
if 'app.py' in line:
print(f" 📋 进程: {' '.join(line.split()[10:])}")
return True
else:
print("❌ 服务器未运行")
print("\n💡 启动服务器:")
print(" cd server && python3 app.py")
return False
except Exception as e:
print(f"⚠️ 无法检查进程: {e}")
return None
def check_files():
"""检查关键文件"""
print("\n" + "=" * 60)
print("🔍 检查关键文件...")
print("=" * 60)
files = {
'frontend/login.html': '登录页面',
'frontend/assets/login.css': '登录样式',
'frontend/js/api.js': 'API 接口',
'server/app.py': '后端服务',
}
all_exist = True
for path, desc in files.items():
if os.path.exists(path):
size = os.path.getsize(path)
print(f"{desc:20s} - {path} ({size} bytes)")
else:
print(f"{desc:20s} - {path} (不存在)")
all_exist = False
return all_exist
def show_instructions():
"""显示使用说明"""
print("\n" + "=" * 60)
print("📖 使用说明")
print("=" * 60)
print("\n1⃣ 启动后端服务:")
print(" cd server")
print(" python3 app.py")
print("\n2⃣ 访问登录页面:")
print(" http://localhost:5000/login.html")
print(" ⚠️ 注意:不是 login-preview.html")
print("\n3⃣ 使用以下账号登录:")
print(" - tz (超级管理员)")
print(" - 张正浩 (超级管理员)")
print(" - admin (管理员)")
print(" - 黄有想 (管理员)")
print("\n4⃣ 如果忘记密码,重置密码:")
print(" python3 reset_password.py <用户名> <新密码>")
print("\n5⃣ 清除浏览器缓存:")
print(" Chrome/Edge: Ctrl+Shift+Delete")
print(" 或者使用无痕模式: Ctrl+Shift+N")
def create_reset_script():
"""创建密码重置脚本"""
script = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
密码重置脚本
用法: python3 reset_password.py <用户名> <新密码>
"""
import sys
import sqlite3
from werkzeug.security import generate_password_hash
if len(sys.argv) != 3:
print("用法: python3 reset_password.py <用户名> <新密码>")
sys.exit(1)
username = sys.argv[1]
new_password = sys.argv[2]
DB_PATH = 'server/data.db'
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# 检查用户是否存在
user = c.execute('SELECT id FROM users WHERE username = ?', (username,)).fetchone()
if not user:
print(f"❌ 用户 '{username}' 不存在")
print("\\n现有用户:")
users = c.execute('SELECT username FROM users').fetchall()
for u in users:
print(f" - {u[0]}")
sys.exit(1)
# 更新密码
password_hash = generate_password_hash(new_password)
c.execute('UPDATE users SET password_hash = ? WHERE username = ?', (password_hash, username))
conn.commit()
print(f"✅ 用户 '{username}' 的密码已重置")
print(f" 新密码: {new_password}")
conn.close()
except Exception as e:
print(f"❌ 错误: {e}")
sys.exit(1)
'''
with open('reset_password.py', 'w', encoding='utf-8') as f:
f.write(script)
os.chmod('reset_password.py', 0o755)
print("\n✅ 已创建密码重置脚本: reset_password.py")
def main():
print("\n" + "🔐 登录问题诊断工具".center(60, "="))
print()
db_ok = check_database()
files_ok = check_files()
server_ok = check_server()
print("\n" + "=" * 60)
print("📊 诊断结果")
print("=" * 60)
if db_ok and files_ok:
print("✅ 数据库和文件都正常")
if server_ok:
print("✅ 服务器正在运行")
print("\n💡 如果仍然无法登录,请尝试:")
print(" 1. 清除浏览器缓存")
print(" 2. 使用无痕模式")
print(" 3. 检查浏览器控制台的错误信息")
print(" 4. 确认访问的是 login.html 而不是 login-preview.html")
else:
print("❌ 服务器未运行,请先启动服务器")
else:
print("❌ 发现问题,请检查上述错误信息")
show_instructions()
create_reset_script()
print("\n" + "=" * 60)
print()
if __name__ == '__main__':
main()

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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,88 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""测试验证码生成功能"""
try:
from PIL import Image, ImageDraw, ImageFont
import random
import io
import base64
print("✓ Pillow 库已安装")
# 生成4位随机数字
code = ''.join([str(random.randint(0, 9)) for _ in range(4)])
print(f"✓ 生成验证码: {code}")
# 创建图片
width, height = 120, 40
image = Image.new('RGB', (width, height), color='#f0f4f8')
draw = ImageDraw.Draw(image)
print("✓ 创建图片成功")
# 尝试使用系统字体
font = None
font_paths = [
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
'/System/Library/Fonts/Helvetica.ttc',
'C:\\Windows\\Fonts\\arial.ttf'
]
for font_path in font_paths:
try:
font = ImageFont.truetype(font_path, 28)
print(f"✓ 使用字体: {font_path}")
break
except:
continue
if not font:
font = ImageFont.load_default()
print("⚠ 使用默认字体")
# 绘制干扰线
for _ in range(3):
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
draw.line([(x1, y1), (x2, y2)], fill='#cbd5e1', width=1)
# 绘制验证码文字
colors = ['#3b82f6', '#2563eb', '#1e40af', '#1e3a8a']
for i, char in enumerate(code):
x = 20 + i * 25 + random.randint(-3, 3)
y = 5 + random.randint(-3, 3)
color = random.choice(colors)
draw.text((x, y), char, font=font, fill=color)
# 绘制干扰点
for _ in range(50):
x = random.randint(0, width)
y = random.randint(0, height)
draw.point((x, y), fill='#94a3b8')
print("✓ 绘制验证码成功")
# 转换为base64
buffer = io.BytesIO()
image.save(buffer, format='PNG')
buffer.seek(0)
img_base64 = base64.b64encode(buffer.getvalue()).decode()
print(f"✓ 转换为base64成功 (长度: {len(img_base64)})")
print("\n验证码功能测试通过!")
# 保存测试图片
image.save('test_captcha.png')
print("✓ 测试图片已保存为 test_captcha.png")
except ImportError as e:
print(f"✗ 缺少依赖库: {e}")
print("\n请安装 Pillow 库:")
print(" pip install Pillow")
except Exception as e:
print(f"✗ 测试失败: {e}")
import traceback
traceback.print_exc()

View File

@ -1,59 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.insert(0, 'server')
# 测试导入函数的逻辑
def test_parse():
# 模拟辅助函数
def parse_percentage(value):
if value is None:
return 0
if isinstance(value, (int, float)):
return float(value)
value_str = str(value).strip()
if value_str.endswith('%'):
value_str = value_str[:-1]
try:
return float(value_str)
except:
return 0
def safe_int(value, default=0):
if value is None:
return default
try:
return int(float(value))
except:
return default
# 测试数据
test_cases = [
('62.28%', 62.28),
('62.28', 62.28),
(62.28, 62.28),
('', 0),
(None, 0),
]
print("测试 parse_percentage:")
for input_val, expected in test_cases:
result = parse_percentage(input_val)
status = "" if result == expected else ""
print(f" {status} parse_percentage({repr(input_val)}) = {result} (期望: {expected})")
print("\n测试 safe_int:")
test_int_cases = [
(100, 100),
(100.5, 100),
('100', 100),
('', 0),
(None, 0),
]
for input_val, expected in test_int_cases:
result = safe_int(input_val)
status = "" if result == expected else ""
print(f" {status} safe_int({repr(input_val)}) = {result} (期望: {expected})")
if __name__ == '__main__':
test_parse()

View File

@ -1,173 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SOP 功能测试脚本
用于验证 SOP 文件管理功能是否正常工作
"""
import os
import sys
import sqlite3
# 添加 server 目录到路径
sys.path.insert(0, 'server')
def test_database_table():
"""测试数据库表是否创建成功"""
print("测试 1: 检查数据库表...")
db_path = 'server/data.db'
if not os.path.exists(db_path):
print(" ❌ 数据库文件不存在")
return False
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 检查 sop_files 表是否存在
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sop_files'")
result = cursor.fetchone()
if result:
print(" ✅ sop_files 表已创建")
# 检查表结构
cursor.execute("PRAGMA table_info(sop_files)")
columns = cursor.fetchall()
print(f" ✅ 表结构包含 {len(columns)} 列:")
for col in columns:
print(f" - {col[1]} ({col[2]})")
conn.close()
return True
else:
print(" ❌ sop_files 表不存在")
conn.close()
return False
def test_sop_directory():
"""测试 SOP 文件存储目录"""
print("\n测试 2: 检查 SOP 文件目录...")
sop_dir = 'frontend/sop_files'
if os.path.exists(sop_dir):
print(f" ✅ 目录已存在: {sop_dir}")
# 检查目录权限
if os.access(sop_dir, os.W_OK):
print(" ✅ 目录可写")
else:
print(" ⚠️ 目录不可写,可能需要调整权限")
# 列出现有文件
files = os.listdir(sop_dir)
if files:
print(f" 📁 目录中已有 {len(files)} 个文件:")
for f in files[:5]: # 只显示前5个
print(f" - {f}")
else:
print(" 📁 目录为空")
return True
else:
print(f" ⚠️ 目录不存在: {sop_dir}")
print(" 💡 系统会在首次上传时自动创建")
return True
def test_api_routes():
"""测试 API 路由是否注册"""
print("\n测试 3: 检查 API 路由...")
try:
from app import app
# 获取所有路由
routes = []
for rule in app.url_map.iter_rules():
if 'sop' in rule.rule:
routes.append(f"{rule.rule} [{', '.join(rule.methods - {'HEAD', 'OPTIONS'})}]")
if routes:
print(" ✅ SOP API 路由已注册:")
for route in routes:
print(f" - {route}")
return True
else:
print(" ❌ 未找到 SOP API 路由")
return False
except Exception as e:
print(f" ❌ 导入失败: {e}")
return False
def test_frontend_files():
"""测试前端文件是否更新"""
print("\n测试 4: 检查前端文件...")
files_to_check = {
'frontend/js/api.js': ['listSopFiles', 'uploadSopFile', 'deleteSopFile'],
'frontend/js/components/upload.js': ['renderSop', 'bindSopEvents', 'loadSopList'],
'frontend/js/router.js': ['sop'],
'frontend/index.html': ['upload/sop']
}
all_ok = True
for filepath, keywords in files_to_check.items():
if not os.path.exists(filepath):
print(f" ❌ 文件不存在: {filepath}")
all_ok = False
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
missing = [kw for kw in keywords if kw not in content]
if missing:
print(f" ⚠️ {filepath} 缺少关键字: {', '.join(missing)}")
all_ok = False
else:
print(f"{filepath} 已更新")
return all_ok
def main():
"""运行所有测试"""
print("=" * 60)
print("SOP 功能测试")
print("=" * 60)
results = []
# 运行测试
results.append(("数据库表", test_database_table()))
results.append(("文件目录", test_sop_directory()))
results.append(("API 路由", test_api_routes()))
results.append(("前端文件", test_frontend_files()))
# 汇总结果
print("\n" + "=" * 60)
print("测试结果汇总")
print("=" * 60)
for name, result in results:
status = "✅ 通过" if result else "❌ 失败"
print(f"{name}: {status}")
all_passed = all(r[1] for r in results)
print("\n" + "=" * 60)
if all_passed:
print("🎉 所有测试通过SOP 功能已准备就绪。")
print("\n下一步:")
print("1. 重启服务器")
print("2. 登录系统(使用管理员账号)")
print("3. 进入 '上传''SOP' 菜单")
print("4. 尝试上传测试文件: sop_template_example.csv")
else:
print("⚠️ 部分测试未通过,请检查上述错误信息。")
print("=" * 60)
return 0 if all_passed else 1
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,162 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试发货单上传解析功能
"""
import pandas as pd
import numpy as np
def test_parse_shipment():
"""测试解析发货单"""
file_path = '/home/hyx/work/生产管理系统/发货单-20251121.xls'
# 读取Excel文件
df = pd.read_excel(file_path, header=None)
print("=== 解析发货单头部信息 ===")
# 提取头部信息
shipment_date = None
transport_method = None
# 解析发货日期第1行索引2
if len(df) > 1 and len(df.columns) > 2:
shipment_date_raw = df.iloc[1, 2]
if pd.notna(shipment_date_raw):
if isinstance(shipment_date_raw, pd.Timestamp):
shipment_date = shipment_date_raw.strftime('%Y-%m-%d')
else:
shipment_date = str(shipment_date_raw)
print(f"发货日期: {shipment_date}")
# 解析供货方式第2行索引2
if len(df) > 2 and len(df.columns) > 2:
transport_method_raw = df.iloc[2, 2]
if pd.notna(transport_method_raw):
transport_method = str(transport_method_raw)
print(f"供货方式(运输单号): {transport_method}")
# 找到表格数据的起始行(序号、采购单号、物料编码...
header_row = None
for i in range(len(df)):
if df.iloc[i, 0] == '序号':
header_row = i
break
if header_row is None:
print("错误:无法识别发货单格式,未找到表格头部")
return
print(f"\n表格起始行: {header_row}")
# 从表格起始行读取数据
data_df = pd.read_excel(file_path, header=header_row)
print(f"表格列名: {data_df.columns.tolist()}")
# 过滤掉合计行和备注行(只保留序号为数字的行)
valid_data = data_df[data_df['序号'].apply(lambda x: isinstance(x, (int, float)) and not pd.isna(x))]
print(f"\n有效数据行数: {len(valid_data)}")
# 模拟客户订单数据
customer_orders = {
'CGDD002878': {
'order_date': '2025-11-15',
'material': 'AP-DZ006 灯条基站',
'unit_price': 150.5
},
'CGDD003082': {
'order_date': '2025-11-18',
'material': '飞机盒',
'unit_price': 5.0
}
}
print("\n=== 解析数据行 ===")
last_contract_no = None
for idx, row in valid_data.iterrows():
print(f"\n序号: {int(row['序号'])}")
# 提取数据
contract_no = row.get('采购单号')
if pd.isna(contract_no):
if last_contract_no:
contract_no = last_contract_no
print(f" 采购单号: {contract_no} (继承上一行)")
else:
print(f" 采购单号: 空 (错误)")
continue
else:
contract_no = str(contract_no).strip()
last_contract_no = contract_no
print(f" 采购单号(合同编号): {contract_no}")
material_code = row.get('物料编码')
if pd.isna(material_code):
print(f" 物料编码: 空 (错误)")
continue
material_code = str(material_code).strip().replace('\n', ' ')
print(f" 物料编码(物料名称): {material_code}")
spec_model = row.get('规格型号')
if pd.isna(spec_model):
spec_model = ''
else:
spec_model = str(spec_model).strip()
print(f" 规格型号: {spec_model}")
quantity = row.get('实送数量')
if pd.isna(quantity):
print(f" 实送数量: 空 (错误)")
continue
quantity = int(float(quantity))
print(f" 实送数量(数量): {quantity}")
unit = row.get('单位')
if pd.isna(unit):
unit = 'pcs'
else:
unit = str(unit).strip()
print(f" 单位: {unit}")
# 从备注中提取运输单号(如果有)
remark = row.get('备注')
transport_no = transport_method or ''
if pd.notna(remark):
remark_str = str(remark).strip()
if remark_str:
transport_no = remark_str
print(f" 运输单号: {transport_no}")
# 从客户订单中查找单价和下单时间
unit_price = 0
order_date = shipment_date or ''
if contract_no in customer_orders:
order_info = customer_orders[contract_no]
# 匹配物料名称
if material_code in order_info['material']:
unit_price = order_info['unit_price']
order_date = order_info['order_date']
print(f" 含税单价: {unit_price} (从客户订单查找)")
print(f" 下单时间: {order_date} (从客户订单查找)")
else:
print(f" 含税单价: {unit_price} (未找到匹配的客户订单)")
print(f" 下单时间: {order_date} (使用发货日期)")
else:
print(f" 含税单价: {unit_price} (未找到对应的采购单号)")
print(f" 下单时间: {order_date} (使用发货日期)")
# 计算含税金额
total_amount = quantity * unit_price
print(f" 含税金额: {total_amount}")
print(f" 交货日期: {shipment_date}")
print(f" 出货日期: {shipment_date}")
if __name__ == '__main__':
test_parse_shipment()

View File

@ -1,120 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toast 测试</title>
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
background: var(--surface-2);
color: var(--text);
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--border);
opacity: 0;
pointer-events: none;
transition: opacity .2s ease;
}
.toast.show {
opacity: 1;
}
button {
margin: 10px;
padding: 10px 20px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Toast 测试页面</h1>
<button onclick="testShort()">短提示 (2秒)</button>
<button onclick="testMedium()">中等提示 (5秒)</button>
<button onclick="testLong()">长提示 (10秒)</button>
<button onclick="testExtraLong()">超长提示 (15秒)</button>
<button onclick="testMultiLine()">多行提示</button>
<div id="toast" class="toast"></div>
<script>
// 存储当前的toast定时器
let toastTimer = null;
function toast(msg, type = 'info', duration = 2000) {
const t = document.getElementById('toast');
// 清除之前的定时器
if (toastTimer) {
clearTimeout(toastTimer);
toastTimer = null;
}
t.textContent = msg;
// 添加类型样式
if (type === 'success') {
t.style.backgroundColor = '#10b981';
t.style.color = 'white';
} else if (type === 'error') {
t.style.backgroundColor = '#ef4444';
t.style.color = 'white';
} else if (type === 'warning') {
t.style.backgroundColor = '#f59e0b';
t.style.color = 'white';
} else {
t.style.backgroundColor = '#3b82f6';
t.style.color = 'white';
}
// 支持多行文本
t.style.whiteSpace = 'pre-line';
// 添加show类来显示toast
t.classList.add('show');
// 调试信息
console.log(`Toast显示: 类型=${type}, 持续时间=${duration}ms, 消息长度=${msg.length}`);
console.log('当前时间:', new Date().toLocaleTimeString());
toastTimer = setTimeout(() => {
t.classList.remove('show');
// 重置样式
t.style.backgroundColor = '';
t.style.color = '';
t.style.whiteSpace = '';
toastTimer = null;
console.log('Toast已隐藏');
console.log('隐藏时间:', new Date().toLocaleTimeString());
}, duration);
}
function testShort() {
toast('这是一个2秒的短提示', 'info', 2000);
}
function testMedium() {
toast('这是一个5秒的中等提示', 'success', 5000);
}
function testLong() {
toast('这是一个10秒的长提示\n可以显示多行文本', 'warning', 10000);
}
function testExtraLong() {
toast('这是一个15秒的超长提示\n用于测试刷新单价功能\n包含详细信息\n1. 第一条信息\n2. 第二条信息\n3. 第三条信息', 'success', 15000);
}
function testMultiLine() {
toast('成功更新 5 条对账单的单价\n\n详细信息\n1. ID=123, 合同=CGDD002878, 物料=AP-DZ006灯条基站, 单价: 200 → 239.2\n2. ID=124, 合同=CGDD002878, 物料=WD1MK0SMD0551蓝牙模块, 单价: 1.0 → 1.1\n3. ID=125, 合同=CGDD002879, 物料=AP-DZ009灯条基站, 单价: 220 → 229.61', 'success', 15000);
}
</script>
</body>
</html>

View File

@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""
将现有期初库存和采购需求数据的工厂字段更新为"友辉"
"""
import sqlite3
DB_PATH = 'server/data.db'
def update_factory():
"""更新现有数据的工厂字段为友辉"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
try:
# 更新期初库存表
c.execute("UPDATE initial_stock SET factory='友辉' WHERE factory IS NULL OR factory=''")
initial_stock_rows = c.rowcount
# 更新采购需求表
c.execute("UPDATE purchase_demand SET factory='友辉' WHERE factory IS NULL OR factory=''")
purchase_demand_rows = c.rowcount
conn.commit()
print(f"✅ 期初库存表:成功更新 {initial_stock_rows} 条记录的工厂字段为'友辉'")
print(f"✅ 采购需求表:成功更新 {purchase_demand_rows} 条记录的工厂字段为'友辉'")
# 查询验证
c.execute("SELECT COUNT(*) FROM initial_stock WHERE factory='友辉'")
initial_count = c.fetchone()[0]
c.execute("SELECT COUNT(*) FROM purchase_demand WHERE factory='友辉'")
demand_count = c.fetchone()[0]
print(f"📊 当前友辉工厂的期初库存记录数: {initial_count}")
print(f"📊 当前友辉工厂的采购需求记录数: {demand_count}")
except Exception as e:
print(f"❌ 更新失败: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == '__main__':
print("开始更新数据的工厂字段...")
print("=" * 50)
update_factory()
print("=" * 50)
print("更新完成!")

Binary file not shown.