This commit is contained in:
zzh 2025-11-23 13:28:53 +08:00
parent daf4530753
commit c4455def3f
6 changed files with 380 additions and 13 deletions

View File

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

View File

@ -74,6 +74,22 @@
</div>
</div>
</div>
<div class="nav-group">
<div class="nav-group-title">生产管理</div>
<div class="nav-item has-children" data-expand="production-mgmt">
<button class="nav-item-btn">
<span class="icon">📋</span>
<span class="text">生产管理</span>
<span class="caret"></span>
</button>
<div class="nav-children" data-parent="production-mgmt">
<a href="#/production-mgmt/work-order" class="nav-child" data-route="production-mgmt-work-order">
<span class="child-icon">📝</span>
<span>生产工单下发中心</span>
</a>
</div>
</div>
</div>
<div class="nav-group">
<div class="nav-group-title">扩展采集</div>
<div class="nav-item has-children" data-expand="collect">
@ -188,6 +204,7 @@
<script src="./js/components/personnel.js"></script>
<script src="./js/components/qa.js"></script>
<script src="./js/components/production.js"></script>
<script src="./js/components/work-order.js"></script>
<script src="./js/components/export.js"></script>
<script src="./js/components/settings.js"></script>
<script src="./js/components/notifications.js"></script>

View File

@ -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(()=>{});

View File

@ -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; // 超级管理员和管理员才显示通知铃铛
}
// 显示通知铃铛

View File

@ -92,6 +92,8 @@ const Router = (() => {
personnel: '人员信息',
qa: '质检报告',
production: '时间记录',
'production-mgmt': '生产管理',
'work-order': '生产工单下发中心',
export: '导出',
settings: '设置'
};

View File

@ -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/<int:order_id>')
@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/<int:order_id>')
@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/<int:order_id>/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()