diff --git a/inventory-backend/app/api/v1/auth.py b/inventory-backend/app/api/v1/auth.py index 4b76de7..853e67b 100644 --- a/inventory-backend/app/api/v1/auth.py +++ b/inventory-backend/app/api/v1/auth.py @@ -249,3 +249,109 @@ def get_my_permissions(): except Exception as e: current_app.logger.error(f"Get Permissions Failed: {str(e)}") return jsonify({'msg': '获取权限失败'}), 500 + + +@auth_bp.route('/me/password', methods=['PUT']) +@jwt_required() +def change_my_password(): + """ + 【新增】自我修改密码接口 + - 无需管理员权限,只需验证 JWT Token 和旧密码是否正确 + - 字段脱敏:不暴露系统角色 + """ + try: + from app.models.system import SysUser + + claims = get_jwt() + user_id = claims.get('sub') + + data = request.get_json() + if not data: + return jsonify({'msg': '无效的请求数据'}), 400 + + old_password = data.get('old_password') + new_password = data.get('new_password') + confirm_password = data.get('confirm_password') + + if not old_password or not new_password or not confirm_password: + return jsonify({'msg': '旧密码、新密码、确认新密码均不能为空'}), 400 + + if new_password != confirm_password: + return jsonify({'msg': '新密码与确认密码不一致'}), 400 + + if len(new_password) < 6: + return jsonify({'msg': '新密码长度不能少于6位'}), 400 + + # 超级管理员(user_id=0)使用硬编码密码 + if user_id == 0: + if old_password != AuthService.SUPER_ADMIN_PASS: + return jsonify({'msg': '旧密码错误'}), 401 + # 超级管理员密码不存入数据库,直接返回成功(IRIS 使用固定密码) + # 注:如果需要支持 IRIS 修改密码,可在此添加特殊逻辑 + return jsonify({'msg': '超级管理员密码由系统管理员管理,当前会话无需修改'}), 200 + + # 普通用户:从数据库验证旧密码 + user = SysUser.query.get(user_id) + if not user: + return jsonify({'msg': '用户不存在'}), 404 + + if not user.check_password(old_password): + return jsonify({'msg': '旧密码错误'}), 401 + + user.set_password(new_password) + db.session.commit() + + return jsonify({'msg': '密码修改成功,请使用新密码重新登录'}), 200 + + except Exception as e: + current_app.logger.error(f"Change Password Failed: {str(e)}") + return jsonify({'msg': f'密码修改失败: {str(e)}'}), 500 + + +@auth_bp.route('/me', methods=['GET']) +@jwt_required() +def get_my_profile(): + """ + 【新增】获取当前登录用户的个人资料(自我查看) + - 只返回姓名/账号和所属部门 + - 严格脱敏:不暴露系统角色字段 + """ + try: + from app.models.system import SysUser + + claims = get_jwt() + user_id = claims.get('sub') + display_name = claims.get('display_name', '') + account_id = claims.get('username', '') + + # 超级管理员(user_id=0) + if user_id == 0: + return jsonify({ + 'msg': '获取成功', + 'data': { + 'id': 0, + 'username': 'IRIS', + 'display_name': '超级管理员(IRIS)', + 'department': 'System', + # 【关键】严格脱敏:不暴露 role 字段 + } + }), 200 + + user = SysUser.query.get(user_id) + if not user: + return jsonify({'msg': '用户不存在'}), 404 + + return jsonify({ + 'msg': '获取成功', + 'data': { + 'id': user.id, + 'username': account_id, + 'display_name': user.username.split('/')[0] if user.username else display_name, + 'department': user.department or '-', + # 【关键】严格脱敏:不暴露 role 字段 + } + }), 200 + + except Exception as e: + current_app.logger.error(f"Get Profile Failed: {str(e)}") + return jsonify({'msg': f'获取个人资料失败: {str(e)}'}), 500 diff --git a/inventory-web/src/App.vue b/inventory-web/src/App.vue index 26292c7..b29a4f9 100644 --- a/inventory-web/src/App.vue +++ b/inventory-web/src/App.vue @@ -1,15 +1,16 @@ @@ -158,7 +339,14 @@ const handleLogout = () => { display: flex; align-items: center; gap: 8px; - cursor: default; + cursor: pointer; + padding: 4px 8px; + border-radius: 8px; + transition: background-color 0.2s; +} + +.user-profile:hover { + background-color: #f0f2f5; } .user-avatar { @@ -171,12 +359,10 @@ const handleLogout = () => { font-weight: 500; } -.logout-btn { - font-weight: 400; - padding: 4px 8px; -} -.logout-btn:hover { - color: #f56c6c !important; +.dropdown-arrow { + font-size: 12px; + color: #909399; + margin-left: 2px; } .app-content { @@ -204,4 +390,41 @@ const handleLogout = () => { display: flex; align-items: center; } + +/* 个人中心弹窗样式 */ +.profile-info-section { + margin-bottom: 8px; +} + +.section-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 600; + color: #303133; + margin-bottom: 12px; +} + +.profile-descriptions .account-hint { + margin-left: 8px; + font-size: 12px; + color: #909399; +} + +.password-form { + margin-top: 8px; +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.confirm-btn { + display: flex; + align-items: center; + gap: 4px; +} diff --git a/inventory-web/src/api/auth.ts b/inventory-web/src/api/auth.ts index 3da6fcb..3c1b137 100644 --- a/inventory-web/src/api/auth.ts +++ b/inventory-web/src/api/auth.ts @@ -49,4 +49,21 @@ export function deleteUser(id: number) { url: `/v1/auth/user/${id}`, method: 'delete' }) +} + +// 【新增】获取当前登录用户的个人资料(只含姓名/账号/部门,严格脱敏) +export function getMyProfile() { + return request({ + url: '/v1/auth/me', + method: 'get' + }) +} + +// 【新增】自我修改密码(验证旧密码,无需管理员权限) +export function changeMyPassword(data: { old_password: string; new_password: string; confirm_password: string }) { + return request({ + url: '/v1/auth/me/password', + method: 'put', + data + }) } \ No newline at end of file diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 01498c9..06bfb88 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -142,6 +142,7 @@ highlight-current-row header-cell-class-name="table-header-gray" @sort-change="handleSortChange" + :key="hasColumnPermission('sn_bn') ? 'sn' : 'nosn'" >