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 @@
@@ -63,22 +151,24 @@ const handleLogout = () => {
@@ -89,9 +179,100 @@ const handleLogout = () => {
+
+
+
+
+
+
+
+
+
+
+ 个人信息
+
+
+
+ {{ profileForm.display_name || '-' }}
+ (账号: {{ profileForm.username || '-' }})
+
+
+ {{ profileForm.department || '-' }}
+
+
+
+
+
+
+ 修改密码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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'"
>
{
const key = `${item.sku || ''}_${item.spec_model || item.spec || ''}`
if (aggMap.has(key)) {
const existing = aggMap.get(key)
- // 累加库存数量
+ // 累加库存数量(原地修改已拷贝的对象,不影响原始数据)
existing.qty_stock = (existing.qty_stock || 0) + (item.qty_stock || 0)
existing.qty_available = (existing.qty_available || 0) + (item.qty_available || 0)
existing.stock_quantity = (existing.stock_quantity || 0) + (item.stock_quantity || 0)
@@ -829,7 +830,9 @@ const displayData = computed(() => {
existing.raw_material_cost = (existing.raw_material_cost || 0) + (item.raw_material_cost || 0)
existing.unit_total_cost = (existing.unit_total_cost || 0) + (item.unit_total_cost || 0)
} else {
- aggMap.set(key, { ...item })
+ // 【关键修复】使用 Object.assign 深拷贝第一条记录的所有字段,
+ // 绝不能只保留 sku 和 qty,否则其他列(公司/名称/规格等)会读不到数据
+ aggMap.set(key, Object.assign({}, item))
}
}
return Array.from(aggMap.values())