diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css
index 3fd370c..d1a3a50 100644
--- a/frontend/assets/styles.css
+++ b/frontend/assets/styles.css
@@ -616,3 +616,71 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
[data-theme="light"] .watermark-text {
opacity: 0.06;
}
+
+/* 工单页面样式 */
+.page-container{max-width:100%;margin:0 auto}
+.page-header{margin-bottom:24px}
+.page-header h1{font-size:24px;font-weight:700;margin:0;color:var(--text)}
+
+.filter-section{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:20px}
+.filter-row{display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap}
+.filter-item{flex:1;min-width:200px;display:flex;flex-direction:column;gap:6px}
+.filter-item label{color:var(--text-2);font-size:13px;font-weight:500}
+.filter-item input,.filter-item select{background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px;font-size:14px}
+.filter-item input:focus,.filter-item select:focus{outline:2px solid var(--primary);border-color:var(--primary)}
+.filter-actions{display:flex;gap:8px;align-items:flex-end}
+.filter-actions .btn{padding:10px 20px;font-size:14px}
+
+.table-container{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-bottom:20px}
+.data-table{width:100%;border-collapse:collapse;font-size:13px}
+.data-table thead{background:var(--surface-2);position:sticky;top:0;z-index:1}
+.data-table th{padding:12px 16px;text-align:left;font-weight:600;color:var(--text);border-bottom:2px solid var(--border);white-space:nowrap}
+.data-table td{padding:12px 16px;border-bottom:1px solid var(--border);color:var(--text-2)}
+.data-table tbody tr:hover{background:rgba(79,140,255,.08)}
+.data-table tbody tr:last-child td{border-bottom:none}
+
+.status-badge{display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:500}
+.status-badge.status-pending{background:rgba(245,158,11,.2);color:#f59e0b}
+.status-badge.status-processing{background:rgba(79,140,255,.2);color:#4f8cff}
+.status-badge.status-completed{background:rgba(34,197,94,.2);color:#22c55e}
+.status-badge.status-cancelled{background:rgba(239,68,68,.2);color:#ef4444}
+
+.btn-icon{background:transparent;border:none;cursor:pointer;font-size:16px;padding:4px 8px;border-radius:6px;transition:background 0.2s}
+.btn-icon:hover{background:rgba(79,140,255,.15)}
+
+.pagination{display:flex;align-items:center;justify-content:center;gap:16px;padding:16px}
+.pagination .btn{min-width:80px}
+.pagination .btn:disabled{opacity:0.5;cursor:not-allowed}
+#page-info{color:var(--text-2);font-size:14px}
+
+/* 弹窗样式 */
+.modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000;backdrop-filter:blur(4px)}
+.modal-content{background:var(--surface);border:1px solid var(--border);border-radius:16px;width:90%;max-width:600px;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3)}
+.modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--border)}
+.modal-header h2{margin:0;font-size:20px;font-weight:700;color:var(--text)}
+.modal-close{background:transparent;border:none;font-size:28px;color:var(--text-2);cursor:pointer;padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:all 0.2s}
+.modal-close:hover{background:rgba(239,68,68,0.15);color:var(--danger)}
+.modal-body{padding:24px;overflow-y:auto;flex:1}
+.modal-footer{display:flex;gap:12px;justify-content:flex-end;padding:16px 24px;border-top:1px solid var(--border)}
+
+/* 表单网格 */
+.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}
+.form-grid .field.full-width{grid-column:1 / -1}
+.field .required{color:var(--danger);margin-left:2px}
+.field textarea{resize:vertical;font-family:inherit}
+
+/* 页面头部样式 */
+.page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
+
+/* 状态徽章 - 已下发为绿色 */
+.status-badge.status-issued{background:rgba(34,197,94,.2);color:#22c55e}
+
+/* 状态徽章 - 已确认为橘黄色 */
+.status-badge.status-confirmed{background:rgba(251,146,60,.2);color:#fb923c}
+
+/* 删除按钮样式 */
+.btn-icon.btn-danger:hover{background:rgba(239,68,68,.15);color:var(--danger)}
+
+/* 确认按钮样式 */
+.btn-icon.btn-confirm{color:#fb923c}
+.btn-icon.btn-confirm:hover{background:rgba(251,146,60,.15);color:#fb923c}
diff --git a/frontend/index.html b/frontend/index.html
index b727597..49b8388 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -74,6 +74,22 @@
+
扩展采集
@@ -188,6 +204,7 @@
+
diff --git a/frontend/js/app.js b/frontend/js/app.js
index 1e176bd..816efe4 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -90,8 +90,8 @@
document.getElementById('overlay').classList.add('hidden');
updateUserDisplay(currentUser);
- // 初始化通知系统(仅对超级管理员)
- if (currentUser && currentUser.role === 'superadmin' && window.NotificationSystem) {
+ // 初始化通知系统(超级管理员和管理员)
+ if (currentUser && (currentUser.role === 'superadmin' || currentUser.role === 'admin') && window.NotificationSystem) {
window.NotificationSystem.init();
}
});
@@ -102,8 +102,8 @@
currentUser = user;
updateUserDisplay(user);
- // 初始化通知系统(仅对超级管理员)
- if (user && user.role === 'superadmin' && window.NotificationSystem) {
+ // 初始化通知系统(超级管理员和管理员)
+ if (user && (user.role === 'superadmin' || user.role === 'admin') && window.NotificationSystem) {
window.NotificationSystem.init();
}
}).catch(()=>{});
diff --git a/frontend/js/components/notifications.js b/frontend/js/components/notifications.js
index 657d64e..3e8cce2 100644
--- a/frontend/js/components/notifications.js
+++ b/frontend/js/components/notifications.js
@@ -47,7 +47,8 @@
'批量上传不良明细文件': '批量上传了不良明细',
'上传返修记录': '上传了返修记录',
'上传良/不良统计': '上传了良/不良统计',
- '上传不良明细': '上传了不良明细'
+ '上传不良明细': '上传了不良明细',
+ '添加工单': '添加了新工单'
};
return actionMap[action] || action;
}
@@ -148,9 +149,9 @@
try {
// 检查用户角色
const user = await API.me();
- if (user.role !== 'superadmin') {
- console.log('[Notifications] 非超级管理员,不显示通知');
- return; // 只有超级管理员才显示通知铃铛
+ if (user.role !== 'superadmin' && user.role !== 'admin') {
+ console.log('[Notifications] 非管理员,不显示通知');
+ return; // 超级管理员和管理员才显示通知铃铛
}
// 显示通知铃铛
diff --git a/frontend/js/router.js b/frontend/js/router.js
index c759fe6..d9ab0db 100644
--- a/frontend/js/router.js
+++ b/frontend/js/router.js
@@ -92,6 +92,8 @@ const Router = (() => {
personnel: '人员信息',
qa: '质检报告',
production: '时间记录',
+ 'production-mgmt': '生产管理',
+ 'work-order': '生产工单下发中心',
export: '导出',
settings: '设置'
};
diff --git a/server/app.py b/server/app.py
index 7f050ab..c65b3d5 100644
--- a/server/app.py
+++ b/server/app.py
@@ -142,6 +142,26 @@ def init_db():
uploader TEXT,
ts TEXT
)''')
+ c.execute('''CREATE TABLE IF NOT EXISTS work_orders(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ factory TEXT NOT NULL,
+ order_no TEXT NOT NULL,
+ product_model TEXT,
+ order_qty INTEGER NOT NULL,
+ production_start_time TEXT,
+ production_end_time TEXT,
+ status TEXT DEFAULT 'issued',
+ status_text TEXT DEFAULT '已下发',
+ remark TEXT,
+ created_by TEXT,
+ created_at TEXT,
+ updated_at TEXT
+ )''')
+ # 为已存在的表添加列(如果不存在)
+ try:
+ c.execute('ALTER TABLE work_orders ADD COLUMN product_model TEXT')
+ except Exception:
+ pass # 列已存在
conn.commit()
# create default admin
c.execute('SELECT id FROM users WHERE username=?', ('admin',))
@@ -219,6 +239,48 @@ def notify_superadmin(action, detail=''):
pass
+def notify_admins(action, detail=''):
+ """为管理员创建通知(超级管理员操作时使用)"""
+ try:
+ user_id = session.get('user_id')
+ if not user_id:
+ return
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 获取当前用户信息
+ c.execute('SELECT username, role FROM users WHERE id=?', (user_id,))
+ user = c.fetchone()
+ if not user:
+ conn.close()
+ return
+
+ # 只有超级管理员的操作才通知管理员
+ if user['role'] != 'superadmin':
+ conn.close()
+ return
+
+ # 为所有管理员创建通知
+ c.execute('SELECT id FROM users WHERE role=?', ('admin',))
+ admins = c.fetchall()
+
+ # 使用北京时间(UTC+8)
+ from datetime import timezone, timedelta
+ beijing_tz = timezone(timedelta(hours=8))
+ now = datetime.now(beijing_tz).isoformat()
+
+ for admin in admins:
+ c.execute('INSERT INTO notifications(user_id, username, action, detail, ts, read) VALUES(?,?,?,?,?,?)', (
+ admin['id'], user['username'], action, detail, now, 0
+ ))
+
+ conn.commit()
+ conn.close()
+ except Exception:
+ pass
+
+
def get_redis():
global _redis_client
if not redis:
@@ -365,6 +427,7 @@ def login():
return jsonify({'error': 'invalid credentials'}), 400
session['user_id'] = row['id']
session['role'] = row['role']
+ session['username'] = username
session.permanent = True
log('login', username)
return jsonify({'ok': True})
@@ -1634,7 +1697,7 @@ def clear_module():
# notifications
@app.get('/api/notifications')
@require_login
-@require_any_role('superadmin')
+@require_any_role('superadmin', 'admin')
def get_notifications():
"""获取当前用户的通知列表"""
user_id = session.get('user_id')
@@ -1648,7 +1711,7 @@ def get_notifications():
@app.get('/api/notifications/unread-count')
@require_login
-@require_any_role('superadmin')
+@require_any_role('superadmin', 'admin')
def get_unread_count():
"""获取未读通知数量"""
user_id = session.get('user_id')
@@ -1662,7 +1725,7 @@ def get_unread_count():
@app.post('/api/notifications/mark-read')
@require_login
-@require_any_role('superadmin')
+@require_any_role('superadmin', 'admin')
def mark_notification_read():
"""标记通知为已读"""
data = request.get_json() or {}
@@ -1682,7 +1745,7 @@ def mark_notification_read():
@app.post('/api/notifications/mark-all-read')
@require_login
-@require_any_role('superadmin')
+@require_any_role('superadmin', 'admin')
def mark_all_notifications_read():
"""标记所有通知为已读"""
user_id = session.get('user_id')
@@ -1696,7 +1759,7 @@ def mark_all_notifications_read():
@app.post('/api/notifications/delete-read')
@require_login
-@require_any_role('superadmin')
+@require_any_role('superadmin', 'admin')
def delete_read_notifications():
"""删除所有已读通知"""
user_id = session.get('user_id')
@@ -2543,6 +2606,222 @@ def delete_sop_file(file_id):
return jsonify({'ok': True, 'message': 'SOP 文件已删除'})
+# ==================== 工单管理 API ====================
+
+@app.get('/api/work-orders')
+@require_login
+def get_work_orders():
+ """获取工单列表"""
+ factory = request.args.get('factory', '')
+ order_no = request.args.get('order', '')
+ date = request.args.get('date', '')
+
+ conn = get_db()
+ c = conn.cursor()
+
+ query = 'SELECT * FROM work_orders WHERE 1=1'
+ params = []
+
+ if factory:
+ query += ' AND factory LIKE ?'
+ params.append(f'%{factory}%')
+
+ if order_no:
+ query += ' AND order_no LIKE ?'
+ params.append(f'%{order_no}%')
+
+ if date:
+ query += ' AND (production_start_time LIKE ? OR production_end_time LIKE ?)'
+ params.append(f'{date}%')
+ params.append(f'{date}%')
+
+ query += ' ORDER BY created_at DESC'
+
+ c.execute(query, params)
+ rows = c.fetchall()
+ conn.close()
+
+ orders = []
+ for row in rows:
+ orders.append({
+ 'id': str(row['id']),
+ 'factory': row['factory'],
+ 'orderNo': row['order_no'],
+ 'productModel': row['product_model'] if 'product_model' in row.keys() else '',
+ 'orderQty': row['order_qty'],
+ 'productionStartTime': row['production_start_time'],
+ 'productionEndTime': row['production_end_time'],
+ 'status': row['status'],
+ 'statusText': row['status_text'],
+ 'remark': row['remark'],
+ 'createdBy': row['created_by'],
+ 'createdAt': row['created_at']
+ })
+
+ return jsonify({'ok': True, 'data': orders})
+
+
+@app.post('/api/work-orders')
+@require_login
+@require_any_role('admin','superadmin')
+def create_work_order():
+ """创建工单"""
+ data = request.get_json()
+
+ factory = data.get('factory', '').strip()
+ order_no = data.get('orderNo', '').strip()
+ product_model = data.get('productModel', '').strip()
+ order_qty = data.get('orderQty', 0)
+ production_start_time = data.get('productionStartTime', '')
+ production_end_time = data.get('productionEndTime', '')
+ remark = data.get('remark', '').strip()
+
+ if not factory or not order_no or not order_qty:
+ return jsonify({'error': '请填写所有必填项'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ username = session.get('username', '')
+ now = get_beijing_time()
+
+ c.execute('''INSERT INTO work_orders(
+ factory, order_no, product_model, order_qty, production_start_time, production_end_time,
+ status, status_text, remark, created_by, created_at, updated_at
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)''', (
+ factory, order_no, product_model, order_qty, production_start_time, production_end_time,
+ 'issued', '已下发', remark, username, now, now
+ ))
+
+ order_id = c.lastrowid
+ conn.commit()
+ conn.close()
+
+ log('create_work_order', f'工单号: {order_no}, 工厂: {factory}, 型号: {product_model}')
+
+ # 如果是超级管理员添加工单,通知所有管理员
+ notify_admins('添加工单', f'工单号: {order_no}, 工厂: {factory}, 数量: {order_qty}')
+
+ return jsonify({'ok': True, 'id': order_id, 'message': '工单创建成功'})
+
+
+@app.put('/api/work-orders/')
+@require_login
+@require_any_role('admin','superadmin')
+def update_work_order(order_id):
+ """更新工单"""
+ data = request.get_json()
+
+ factory = data.get('factory', '').strip()
+ order_no = data.get('orderNo', '').strip()
+ product_model = data.get('productModel', '').strip()
+ order_qty = data.get('orderQty', 0)
+ production_start_time = data.get('productionStartTime', '')
+ production_end_time = data.get('productionEndTime', '')
+ remark = data.get('remark', '').strip()
+
+ if not factory or not order_no or not order_qty:
+ return jsonify({'error': '请填写所有必填项'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 检查工单是否存在
+ c.execute('SELECT id FROM work_orders WHERE id=?', (order_id,))
+ if not c.fetchone():
+ conn.close()
+ return jsonify({'error': '工单不存在'}), 404
+
+ now = get_beijing_time()
+
+ c.execute('''UPDATE work_orders SET
+ factory=?, order_no=?, product_model=?, order_qty=?, production_start_time=?, production_end_time=?,
+ remark=?, updated_at=?
+ WHERE id=?''', (
+ factory, order_no, product_model, order_qty, production_start_time, production_end_time,
+ remark, now, order_id
+ ))
+
+ conn.commit()
+ conn.close()
+
+ log('update_work_order', f'工单ID: {order_id}, 工单号: {order_no}, 型号: {product_model}')
+ notify_superadmin('更新工单', f'工单号: {order_no}, 工厂: {factory}, 型号: {product_model}')
+
+ return jsonify({'ok': True, 'message': '工单更新成功'})
+
+
+@app.delete('/api/work-orders/')
+@require_login
+@require_any_role('admin','superadmin')
+def delete_work_order(order_id):
+ """删除工单"""
+ conn = get_db()
+ c = conn.cursor()
+
+ # 获取工单信息用于日志
+ c.execute('SELECT order_no, factory FROM work_orders WHERE id=?', (order_id,))
+ row = c.fetchone()
+
+ if not row:
+ conn.close()
+ return jsonify({'error': '工单不存在'}), 404
+
+ order_no = row['order_no']
+ factory = row['factory']
+
+ c.execute('DELETE FROM work_orders WHERE id=?', (order_id,))
+ conn.commit()
+ conn.close()
+
+ log('delete_work_order', f'工单ID: {order_id}, 工单号: {order_no}')
+ notify_superadmin('删除工单', f'工单号: {order_no}, 工厂: {factory}')
+
+ return jsonify({'ok': True, 'message': '工单删除成功'})
+
+
+@app.post('/api/work-orders//confirm')
+@require_login
+@require_any_role('admin','superadmin')
+def confirm_work_order(order_id):
+ """确认工单"""
+ conn = get_db()
+ c = conn.cursor()
+
+ # 检查工单是否存在
+ c.execute('SELECT order_no, factory, status FROM work_orders WHERE id=?', (order_id,))
+ row = c.fetchone()
+
+ if not row:
+ conn.close()
+ return jsonify({'error': '工单不存在'}), 404
+
+ order_no = row['order_no']
+ factory = row['factory']
+ current_status = row['status']
+
+ # 如果已经确认,返回提示
+ if current_status == 'confirmed':
+ conn.close()
+ return jsonify({'ok': True, 'message': '工单已确认'})
+
+ # 更新状态为已确认
+ now = get_beijing_time()
+ c.execute('''UPDATE work_orders SET
+ status=?, status_text=?, updated_at=?
+ WHERE id=?''', (
+ 'confirmed', '已确认', now, order_id
+ ))
+
+ conn.commit()
+ conn.close()
+
+ log('confirm_work_order', f'工单ID: {order_id}, 工单号: {order_no}')
+ notify_superadmin('确认工单', f'工单号: {order_no}, 工厂: {factory}')
+
+ return jsonify({'ok': True, 'message': '工单确认成功'})
+
+
init_db()