界面优化
This commit is contained in:
parent
03358b5426
commit
bd27a69944
13
.env.example
13
.env.example
@ -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
66
.gitignore
vendored
Executable file → Normal 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
|
||||
|
||||
BIN
.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc
Executable file
BIN
.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc
Executable file
Binary file not shown.
2
.vscode/settings.json
vendored
Executable file
2
.vscode/settings.json
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
Binary file not shown.
@ -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响应状态和错误消息
|
||||
@ -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 ✅
|
||||
|
||||
## 其他修复
|
||||
- ✅ 期初库存导入:支持同一物料在不同工厂独立存在
|
||||
- ✅ 数据备份恢复:导入/删除前自动备份,可一键恢复
|
||||
@ -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
|
||||
```
|
||||
@ -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()
|
||||
BIN
backend/__pycache__/ai_service.cpython-312.pyc
Executable file
BIN
backend/__pycache__/ai_service.cpython-312.pyc
Executable file
Binary file not shown.
@ -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()
|
||||
@ -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
569
frontend/assets/login.css.bak
Executable 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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
494
frontend/js/components/ai-report.js.bak
Executable file
494
frontend/js/components/ai-report.js.bak
Executable 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();
|
||||
@ -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
268
frontend/login.html.bak
Executable 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>
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
BIN
server/__pycache__/ai_routes.cpython-312.pyc
Executable file
BIN
server/__pycache__/ai_routes.cpython-312.pyc
Executable file
Binary file not shown.
BIN
server/__pycache__/app.cpython-312.pyc
Executable file
BIN
server/__pycache__/app.cpython-312.pyc
Executable file
Binary file not shown.
BIN
server/__pycache__/finance_routes.cpython-312.pyc
Normal file
BIN
server/__pycache__/finance_routes.cpython-312.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/gunicorn_config.cpython-312.pyc
Executable file
BIN
server/__pycache__/gunicorn_config.cpython-312.pyc
Executable file
Binary file not shown.
@ -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
BIN
server/app.pyc
Executable file
Binary file not shown.
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
@ -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>
|
||||
@ -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>
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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("测试完成!")
|
||||
@ -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()
|
||||
@ -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 |
@ -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()
|
||||
@ -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()
|
||||
@ -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())
|
||||
@ -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()
|
||||
120
test_toast.html
120
test_toast.html
@ -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>
|
||||
@ -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("更新完成!")
|
||||
BIN
发货单-20251121.xls
BIN
发货单-20251121.xls
Binary file not shown.
Loading…
Reference in New Issue
Block a user