📋 用户列表
@@ -277,16 +302,22 @@ Router.register('/settings', async () => {
addUserBtn?.addEventListener('click', async () => {
const usernameEl = document.getElementById('new-username');
const passwordEl = document.getElementById('new-password');
+ const factoryEl = document.getElementById('new-factory');
const roleEl = document.getElementById('new-role');
const username = usernameEl?.value?.trim();
const password = passwordEl?.value;
+ const factory = factoryEl?.value?.trim();
const role = roleEl?.value || 'admin';
if (!username || !password) {
return API.toast('请输入用户名和密码');
}
+ if (!factory) {
+ return API.toast('请输入所属工厂');
+ }
+
if (password.length < 6) {
return API.toast('密码长度至少6位');
}
@@ -297,7 +328,7 @@ Router.register('/settings', async () => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
- body: JSON.stringify({ username, password, role })
+ body: JSON.stringify({ username, password, factory, role })
});
const data = await res.json();
@@ -307,6 +338,7 @@ Router.register('/settings', async () => {
// 清空输入框
if (usernameEl) usernameEl.value = '';
if (passwordEl) passwordEl.value = '';
+ if (factoryEl) factoryEl.value = '';
// 刷新页面以更新用户列表
setTimeout(() => Router.navigate('/settings'), 1000);
} else {
@@ -337,6 +369,57 @@ Router.register('/settings', async () => {
change.disabled = false;
}
});
+
+ // 更新用户工厂
+ const factoryUserSelect = document.getElementById('factory-user');
+ const factoryNameInput = document.getElementById('factory-name');
+
+ // 当选择用户时,自动填充当前工厂
+ factoryUserSelect?.addEventListener('change', () => {
+ const selectedOption = factoryUserSelect.options[factoryUserSelect.selectedIndex];
+ const currentFactory = selectedOption.getAttribute('data-factory') || '';
+ if (factoryNameInput) {
+ factoryNameInput.value = currentFactory;
+ }
+ });
+
+ const updateFactoryBtn = document.getElementById('update-factory-btn');
+ updateFactoryBtn?.addEventListener('click', async () => {
+ const username = factoryUserSelect?.value;
+ const factory = factoryNameInput?.value?.trim();
+
+ if (!username) {
+ return API.toast('请选择用户');
+ }
+
+ if (!factory) {
+ return API.toast('请输入工厂名称');
+ }
+
+ updateFactoryBtn.disabled = true;
+ try {
+ const res = await fetch('/api/admin/update-user-factory', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ username, factory })
+ });
+
+ const data = await res.json();
+
+ if (res.ok && data.ok) {
+ API.toast(data.message || '工厂更新成功');
+ // 刷新页面以更新用户列表
+ setTimeout(() => Router.navigate('/settings'), 1000);
+ } else {
+ API.toast(data.error || '更新失败');
+ }
+ } catch(e) {
+ API.toast('更新失败:' + e.message);
+ } finally {
+ updateFactoryBtn.disabled = false;
+ }
+ });
// 水印开关
const watermarkToggle = document.getElementById('watermark-toggle');
diff --git a/frontend/js/components/work-order.js b/frontend/js/components/work-order.js
index 8e79b82..6c24a16 100644
--- a/frontend/js/components/work-order.js
+++ b/frontend/js/components/work-order.js
@@ -116,6 +116,8 @@
Router.register('/production-mgmt/work-order', async () => {
const html = await render();
setTimeout(async () => {
+ // 等待DOM完全渲染
+ await new Promise(resolve => setTimeout(resolve, 50));
await initPage();
}, 0);
return html;
@@ -249,6 +251,12 @@
const prevBtn = document.getElementById('prev-page');
const nextBtn = document.getElementById('next-page');
+ // 检查DOM元素是否存在
+ if (!tbody || !pageInfo || !prevBtn || !nextBtn) {
+ console.warn('工单页面DOM元素未就绪,跳过加载');
+ return;
+ }
+
// 获取筛选条件
const factory = document.getElementById('factory-filter')?.value || '';
const order = document.getElementById('order-filter')?.value || '';
@@ -346,6 +354,7 @@
const modal = document.getElementById('work-order-modal');
const modalTitle = document.getElementById('modal-title');
const form = document.getElementById('work-order-form');
+ const factoryInput = document.getElementById('form-factory');
form.reset();
@@ -353,7 +362,7 @@
// 编辑模式
modalTitle.textContent = '编辑工单';
document.getElementById('form-order-id').value = order.id;
- document.getElementById('form-factory').value = order.factory;
+ factoryInput.value = order.factory;
document.getElementById('form-order-no').value = order.orderNo;
document.getElementById('form-product-model').value = order.productModel || '';
document.getElementById('form-order-qty').value = order.orderQty;
@@ -364,6 +373,18 @@
// 添加模式
modalTitle.textContent = '添加工单';
document.getElementById('form-order-id').value = '';
+
+ // 如果是管理员,自动填充工厂并设为只读
+ if (currentUser && currentUser.role === 'admin' && currentUser.factory) {
+ factoryInput.value = currentUser.factory;
+ factoryInput.readOnly = true;
+ factoryInput.style.backgroundColor = 'var(--surface)';
+ factoryInput.style.cursor = 'not-allowed';
+ } else {
+ factoryInput.readOnly = false;
+ factoryInput.style.backgroundColor = '';
+ factoryInput.style.cursor = '';
+ }
}
modal.style.display = 'flex';
diff --git a/frontend/js/router.js b/frontend/js/router.js
index 344d35a..ef63576 100644
--- a/frontend/js/router.js
+++ b/frontend/js/router.js
@@ -85,6 +85,7 @@ const Router = (() => {
'work-order': '生产工单下发中心',
'plan-mgmt': '计划管理',
'material-purchase': '物料清单-采购',
+ 'customer-order': '客户订单',
export: '导出',
settings: '设置'
};
diff --git a/server/app.py b/server/app.py
index 8a20ade..5256b6a 100644
--- a/server/app.py
+++ b/server/app.py
@@ -81,6 +81,10 @@ def init_db():
c.execute('ALTER TABLE users ADD COLUMN avatar TEXT')
except Exception:
pass # 列已存在
+ try:
+ c.execute('ALTER TABLE users ADD COLUMN factory TEXT')
+ except Exception:
+ pass # 列已存在
try:
c.execute('ALTER TABLE mac_batches ADD COLUMN platform TEXT DEFAULT "pdd"')
except Exception:
@@ -184,6 +188,23 @@ def init_db():
deleted INTEGER DEFAULT 0,
deleted_at TEXT
)''')
+ c.execute('''CREATE TABLE IF NOT EXISTS customer_orders(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ order_date TEXT NOT NULL,
+ order_no TEXT NOT NULL,
+ customer_name TEXT NOT NULL,
+ material TEXT NOT NULL,
+ quantity INTEGER NOT NULL,
+ unit_price REAL NOT NULL,
+ created_by TEXT,
+ created_at TEXT,
+ updated_at TEXT
+ )''')
+ # 为已存在的表添加列(如果不存在)
+ try:
+ c.execute('ALTER TABLE customer_orders ADD COLUMN customer_name TEXT')
+ except Exception:
+ pass # 列已存在
# 为已存在的表添加列(如果不存在)
try:
c.execute('ALTER TABLE work_orders ADD COLUMN product_model TEXT')
@@ -308,6 +329,52 @@ def notify_admins(action, detail=''):
pass
+def notify_admins_by_factory(action, detail='', factory=None):
+ """为指定工厂的管理员创建通知(超级管理员操作时使用)"""
+ 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
+
+ # 如果指定了工厂,只通知该工厂的管理员;否则通知所有管理员
+ if factory:
+ c.execute('SELECT id FROM users WHERE role=? AND factory=?', ('admin', factory))
+ else:
+ 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:
@@ -547,13 +614,18 @@ def login():
def me():
uid = session.get('user_id')
if not uid:
- return jsonify({'username': None, 'role': None, 'avatar': None})
+ return jsonify({'username': None, 'role': None, 'avatar': None, 'factory': None})
conn = get_db()
c = conn.cursor()
- c.execute('SELECT username, role, avatar FROM users WHERE id=?', (uid,))
+ c.execute('SELECT username, role, avatar, factory FROM users WHERE id=?', (uid,))
row = c.fetchone()
conn.close()
- return jsonify({'username': row['username'], 'role': row['role'], 'avatar': row['avatar'] if row['avatar'] else None})
+ return jsonify({
+ 'username': row['username'],
+ 'role': row['role'],
+ 'avatar': row['avatar'] if row['avatar'] else None,
+ 'factory': row['factory'] if row['factory'] else None
+ })
@app.post('/api/auth/logout')
@@ -1623,7 +1695,7 @@ def list_shipments():
def list_users():
conn = get_db()
c = conn.cursor()
- c.execute('SELECT username, role FROM users ORDER BY id ASC')
+ c.execute('SELECT username, role, factory FROM users ORDER BY id ASC')
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({'list': rows})
@@ -1675,6 +1747,37 @@ def change_password():
return jsonify({'ok': True})
+@app.post('/api/admin/update-user-factory')
+@require_login
+@require_any_role('superadmin')
+def update_user_factory():
+ """更新用户所属工厂"""
+ data = request.get_json() or {}
+ username = data.get('username')
+ factory = (data.get('factory') or '').strip()
+
+ if not username:
+ return jsonify({'error': '用户名不能为空'}), 400
+
+ if not factory:
+ return jsonify({'error': '工厂名称不能为空'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('SELECT id FROM users WHERE username=?', (username,))
+ row = c.fetchone()
+ if not row:
+ conn.close()
+ return jsonify({'error': '用户不存在'}), 404
+
+ c.execute('UPDATE users SET factory=? WHERE id=?', (factory, row['id']))
+ conn.commit()
+ conn.close()
+
+ log('update_user_factory', f'username={username}, factory={factory}')
+ return jsonify({'ok': True, 'message': f'已更新用户 {username} 的所属工厂为 {factory}'})
+
+
@app.post('/api/admin/add-user')
@require_login
@require_any_role('superadmin')
@@ -1684,10 +1787,14 @@ def add_user():
username = (data.get('username') or '').strip()
password = data.get('password')
role = (data.get('role') or 'admin').strip()
+ factory = (data.get('factory') or '').strip()
if not username or not password:
return jsonify({'error': '用户名和密码不能为空'}), 400
+ if not factory:
+ return jsonify({'error': '所属工厂不能为空'}), 400
+
if role not in ['admin', 'superadmin']:
return jsonify({'error': '角色必须是 admin 或 superadmin'}), 400
@@ -1702,12 +1809,12 @@ def add_user():
# 创建新用户
try:
- c.execute('INSERT INTO users(username, password_hash, role) VALUES(?,?,?)',
- (username, generate_password_hash(password), role))
+ c.execute('INSERT INTO users(username, password_hash, role, factory) VALUES(?,?,?,?)',
+ (username, generate_password_hash(password), role, factory))
conn.commit()
conn.close()
- log('add_user', f'username={username}, role={role}')
+ log('add_user', f'username={username}, role={role}, factory={factory}')
return jsonify({'ok': True, 'message': f'用户 {username} 创建成功'})
except Exception as e:
conn.close()
@@ -2780,12 +2887,26 @@ def get_work_orders():
order_no = request.args.get('order', '')
date = request.args.get('date', '')
+ # 获取当前用户信息
+ user_id = session.get('user_id')
+ user_role = session.get('role')
+
conn = get_db()
c = conn.cursor()
+ # 获取用户所属工厂
+ c.execute('SELECT factory FROM users WHERE id=?', (user_id,))
+ user_row = c.fetchone()
+ user_factory = user_row['factory'] if user_row and user_row['factory'] else None
+
query = 'SELECT * FROM work_orders WHERE 1=1'
params = []
+ # 如果是管理员(非超级管理员),只能看到自己工厂的订单
+ if user_role == 'admin' and user_factory:
+ query += ' AND factory = ?'
+ params.append(user_factory)
+
if factory:
query += ' AND factory LIKE ?'
params.append(f'%{factory}%')
@@ -2840,13 +2961,28 @@ def create_work_order():
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
+ # 获取当前用户信息
+ user_id = session.get('user_id')
+ user_role = session.get('role')
+ username = session.get('username', '')
conn = get_db()
c = conn.cursor()
- username = session.get('username', '')
+ # 如果是管理员(非超级管理员),强制使用用户自己的工厂
+ if user_role == 'admin':
+ c.execute('SELECT factory FROM users WHERE id=?', (user_id,))
+ user_row = c.fetchone()
+ if user_row and user_row['factory']:
+ factory = user_row['factory']
+ else:
+ conn.close()
+ return jsonify({'error': '您的账户未设置所属工厂,请联系超级管理员'}), 400
+
+ if not factory or not order_no or not order_qty:
+ conn.close()
+ return jsonify({'error': '请填写所有必填项'}), 400
+
now = get_beijing_time()
c.execute('''INSERT INTO work_orders(
@@ -2863,17 +2999,17 @@ def create_work_order():
log('create_work_order', f'工单号: {order_no}, 工厂: {factory}, 型号: {product_model}')
- # 如果是超级管理员添加工单,通知所有管理员
- notify_admins('添加工单', f'工单号: {order_no}, 工厂: {factory}, 数量: {order_qty}')
+ # 如果是超级管理员添加工单,通知该工厂的管理员
+ notify_admins_by_factory('添加工单', f'工单号: {order_no}, 工厂: {factory}, 数量: {order_qty}', factory=factory)
return jsonify({'ok': True, 'id': order_id, 'message': '工单创建成功'})
@app.put('/api/work-orders/
')
@require_login
-@require_any_role('admin','superadmin')
+@require_any_role('superadmin')
def update_work_order(order_id):
- """更新工单"""
+ """更新工单(仅超级管理员)"""
data = request.get_json()
factory = data.get('factory', '').strip()
@@ -2910,16 +3046,16 @@ def update_work_order(order_id):
conn.close()
log('update_work_order', f'工单ID: {order_id}, 工单号: {order_no}, 型号: {product_model}')
- notify_superadmin('更新工单', f'工单号: {order_no}, 工厂: {factory}, 型号: {product_model}')
+ notify_admins_by_factory('更新工单', f'工单号: {order_no}, 工厂: {factory}, 型号: {product_model}', factory=factory)
return jsonify({'ok': True, 'message': '工单更新成功'})
@app.delete('/api/work-orders/')
@require_login
-@require_any_role('admin','superadmin')
+@require_any_role('superadmin')
def delete_work_order(order_id):
- """删除工单"""
+ """删除工单(仅超级管理员)"""
conn = get_db()
c = conn.cursor()
@@ -2939,7 +3075,7 @@ def delete_work_order(order_id):
conn.close()
log('delete_work_order', f'工单ID: {order_id}, 工单号: {order_no}')
- notify_superadmin('删除工单', f'工单号: {order_no}, 工厂: {factory}')
+ notify_admins_by_factory('删除工单', f'工单号: {order_no}, 工厂: {factory}', factory=factory)
return jsonify({'ok': True, 'message': '工单删除成功'})
@@ -2949,6 +3085,10 @@ def delete_work_order(order_id):
@require_any_role('admin','superadmin')
def confirm_work_order(order_id):
"""确认工单"""
+ # 获取当前用户信息
+ user_id = session.get('user_id')
+ user_role = session.get('role')
+
conn = get_db()
c = conn.cursor()
@@ -2964,6 +3104,16 @@ def confirm_work_order(order_id):
factory = row['factory']
current_status = row['status']
+ # 如果是管理员(非超级管理员),检查工单是否属于自己的工厂
+ if user_role == 'admin':
+ c.execute('SELECT factory FROM users WHERE id=?', (user_id,))
+ user_row = c.fetchone()
+ user_factory = user_row['factory'] if user_row and user_row['factory'] else None
+
+ if not user_factory or factory != user_factory:
+ conn.close()
+ return jsonify({'error': '您只能确认自己工厂的工单'}), 403
+
# 如果已经确认,返回提示
if current_status == 'confirmed':
conn.close()
@@ -3020,6 +3170,7 @@ def convert_material_purchase_to_camel(row):
@app.get('/api/material-purchase/list')
@require_login
+@require_any_role('superadmin')
def list_material_purchase():
"""获取物料清单列表(不包括已删除的)"""
conn = get_db()
@@ -3034,6 +3185,7 @@ def list_material_purchase():
@app.get('/api/material-purchase/recycle-bin')
@require_login
+@require_any_role('superadmin')
def list_material_purchase_recycle_bin():
"""获取回收站列表"""
conn = get_db()
@@ -3048,7 +3200,7 @@ def list_material_purchase_recycle_bin():
@app.post('/api/material-purchase/add')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def add_material_purchase():
"""新增物料需求"""
data = request.get_json() or {}
@@ -3089,7 +3241,7 @@ def add_material_purchase():
@app.post('/api/material-purchase/update')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def update_material_purchase():
"""更新物料需求"""
data = request.get_json() or {}
@@ -3126,7 +3278,7 @@ def update_material_purchase():
@app.post('/api/material-purchase/delete')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def delete_material_purchase():
"""删除物料需求(移到回收站)"""
data = request.get_json() or {}
@@ -3153,7 +3305,7 @@ def delete_material_purchase():
@app.post('/api/material-purchase/restore')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def restore_material_purchase():
"""从回收站恢复"""
data = request.get_json() or {}
@@ -3178,7 +3330,7 @@ def restore_material_purchase():
@app.post('/api/material-purchase/permanent-delete')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def permanent_delete_material_purchase():
"""永久删除"""
data = request.get_json() or {}
@@ -3201,7 +3353,7 @@ def permanent_delete_material_purchase():
@app.post('/api/material-purchase/empty-recycle-bin')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def empty_recycle_bin():
"""清空回收站"""
conn = get_db()
@@ -3219,7 +3371,7 @@ def empty_recycle_bin():
@app.route('/api/validate/material-purchase-file', methods=['POST'])
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def validate_material_purchase_file():
"""验证物料清单Excel文件格式"""
f = request.files.get('file')
@@ -3274,7 +3426,7 @@ def validate_material_purchase_file():
@app.post('/api/upload/material-purchase-file')
@require_login
-@require_any_role('admin', 'superadmin')
+@require_any_role('superadmin')
def upload_material_purchase_file():
"""上传物料清单Excel文件"""
f = request.files.get('file')
@@ -3398,6 +3550,115 @@ def upload_material_purchase_file():
return jsonify({'error': f'导入失败:{str(e)}'}), 500
+# ==================== 客户订单 API ====================
+
+@app.get('/api/customer-orders')
+@require_login
+@require_any_role('superadmin')
+def get_customer_orders():
+ """获取客户订单列表"""
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('''SELECT id, order_date, order_no, customer_name, material, quantity, unit_price,
+ created_by, created_at, updated_at
+ FROM customer_orders
+ ORDER BY order_date DESC, id DESC''')
+ rows = c.fetchall()
+ conn.close()
+
+ orders = []
+ for row in rows:
+ orders.append({
+ 'id': row['id'],
+ 'order_date': row['order_date'],
+ 'order_no': row['order_no'],
+ 'customer_name': row['customer_name'] if 'customer_name' in row.keys() else '',
+ 'material': row['material'],
+ 'quantity': row['quantity'],
+ 'unit_price': row['unit_price'],
+ 'created_by': row['created_by'],
+ 'created_at': row['created_at'],
+ 'updated_at': row['updated_at']
+ })
+
+ return jsonify({'list': orders})
+
+
+@app.post('/api/customer-orders')
+@require_login
+@require_any_role('superadmin')
+def create_customer_order():
+ """创建客户订单"""
+ data = request.get_json() or {}
+
+ order_date = data.get('order_date', '').strip()
+ order_no = data.get('order_no', '').strip()
+ customer_name = data.get('customer_name', '').strip()
+ material = data.get('material', '').strip()
+ quantity = data.get('quantity', 0)
+ unit_price = data.get('unit_price', 0)
+
+ if not order_date or not order_no or not customer_name or not material:
+ return jsonify({'error': '请填写所有必填项'}), 400
+
+ if quantity <= 0:
+ return jsonify({'error': '订单数量必须大于0'}), 400
+
+ if unit_price < 0:
+ return jsonify({'error': '单价不能为负数'}), 400
+
+ username = session.get('username', '')
+ now = get_beijing_time()
+
+ conn = get_db()
+ c = conn.cursor()
+
+ c.execute('''INSERT INTO customer_orders(
+ order_date, order_no, customer_name, material, quantity, unit_price,
+ created_by, created_at, updated_at
+ ) VALUES(?,?,?,?,?,?,?,?,?)''', (
+ order_date, order_no, customer_name, material, quantity, unit_price,
+ username, now, now
+ ))
+
+ order_id = c.lastrowid
+ conn.commit()
+ conn.close()
+
+ log('create_customer_order', f'客户: {customer_name}, 订单号: {order_no}, 物料: {material}, 数量: {quantity}')
+
+ return jsonify({'ok': True, 'id': order_id, 'message': '订单创建成功'})
+
+
+@app.delete('/api/customer-orders/')
+@require_login
+@require_any_role('superadmin')
+def delete_customer_order(order_id):
+ """删除客户订单"""
+ conn = get_db()
+ c = conn.cursor()
+
+ # 获取订单信息用于日志
+ c.execute('SELECT order_no, customer_name, material FROM customer_orders WHERE id=?', (order_id,))
+ row = c.fetchone()
+
+ if not row:
+ conn.close()
+ return jsonify({'error': '订单不存在'}), 404
+
+ order_no = row['order_no']
+ customer_name = row['customer_name'] if 'customer_name' in row.keys() else ''
+ material = row['material']
+
+ c.execute('DELETE FROM customer_orders WHERE id=?', (order_id,))
+ conn.commit()
+ conn.close()
+
+ log('delete_customer_order', f'订单ID: {order_id}, 客户: {customer_name}, 订单号: {order_no}')
+
+ return jsonify({'ok': True, 'message': '订单删除成功'})
+
+
@app.errorhandler(404)
def not_found(e):
# 如果请求的是 HTML 页面(通过 Accept header 判断),返回 index.html