Compare commits
2 Commits
f701ed7fc8
...
719fe108ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 719fe108ba | |||
| ec5331ffb3 |
@ -249,3 +249,102 @@ def get_my_permissions():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Get Permissions Failed: {str(e)}")
|
current_app.logger.error(f"Get Permissions Failed: {str(e)}")
|
||||||
return jsonify({'msg': '获取权限失败'}), 500
|
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
|
||||||
|
|
||||||
|
new_password = data.get('new_password')
|
||||||
|
confirm_password = data.get('confirm_password')
|
||||||
|
|
||||||
|
if 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:
|
||||||
|
return jsonify({'msg': '超级管理员密码由系统管理员管理,当前会话无法修改'}), 200
|
||||||
|
|
||||||
|
# 普通用户:JWT 已证明身份,直接更新新密码
|
||||||
|
user = SysUser.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return jsonify({'msg': '用户不存在'}), 404
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import { InfoFilled, SwitchButton, UserFilled } from '@element-plus/icons-vue'
|
import { InfoFilled, SwitchButton, UserFilled, Lock, User, ArrowDown } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { getMyProfile, changeMyPassword } from '@/api/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute() // [新增] 获取当前路由对象
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// [新增] 计算属性:判断当前是否是登录页
|
// 计算属性:判断当前是否是登录页
|
||||||
const isLoginPage = computed(() => {
|
const isLoginPage = computed(() => {
|
||||||
return route.path === '/login'
|
return route.path === '/login'
|
||||||
})
|
})
|
||||||
@ -21,7 +22,102 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- 退出登录逻辑 Start ---
|
// ================================================================
|
||||||
|
// 个人中心 / 修改密码
|
||||||
|
// ================================================================
|
||||||
|
const profileDialogVisible = ref(false)
|
||||||
|
const profileLoading = ref(false)
|
||||||
|
const passwordLoading = ref(false)
|
||||||
|
|
||||||
|
interface ProfileData {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
display_name: string
|
||||||
|
department: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileForm = ref<ProfileData>({
|
||||||
|
id: 0,
|
||||||
|
username: '',
|
||||||
|
display_name: '',
|
||||||
|
department: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordForm = ref({
|
||||||
|
new_password: '',
|
||||||
|
confirm_password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordFormRef = ref()
|
||||||
|
|
||||||
|
// 打开个人中心弹窗
|
||||||
|
const openProfileDialog = async () => {
|
||||||
|
profileDialogVisible.value = true
|
||||||
|
profileLoading.value = true
|
||||||
|
passwordForm.value = { new_password: '', confirm_password: '' }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 【修复】axios 拦截器已解包 response.data,
|
||||||
|
// res 本身已是 { msg, data: { id, username, display_name, department } },
|
||||||
|
// 故直接取 res.data 即可,多跳一层 res.data.data 会取到 undefined
|
||||||
|
const res: any = await getMyProfile()
|
||||||
|
const payload = res.data || res
|
||||||
|
if (payload && payload.data) {
|
||||||
|
profileForm.value = payload.data
|
||||||
|
} else if (payload && payload.username) {
|
||||||
|
// 兜底:响应已经是平铺结构
|
||||||
|
profileForm.value = payload
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || '获取个人资料失败')
|
||||||
|
} finally {
|
||||||
|
profileLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交修改密码(无需旧密码,JWT 已证明身份)
|
||||||
|
const submitPasswordChange = async () => {
|
||||||
|
const { new_password, confirm_password } = passwordForm.value
|
||||||
|
|
||||||
|
if (!new_password || !confirm_password) {
|
||||||
|
ElMessage.warning('请填写新密码和确认密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_password.length < 6) {
|
||||||
|
ElMessage.warning('新密码长度不能少于6位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_password !== confirm_password) {
|
||||||
|
ElMessage.warning('新密码与确认密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordLoading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await changeMyPassword({
|
||||||
|
new_password,
|
||||||
|
confirm_password
|
||||||
|
})
|
||||||
|
const msg = res?.msg || '修改成功'
|
||||||
|
ElMessage.success(msg)
|
||||||
|
profileDialogVisible.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
userStore.logout()
|
||||||
|
router.replace('/login')
|
||||||
|
}, 1500)
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e?.response?.data?.msg || e?.message || '修改失败'
|
||||||
|
ElMessage.error(errMsg)
|
||||||
|
} finally {
|
||||||
|
passwordLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// 退出登录
|
||||||
|
// ================================================================
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
'确定要退出系统吗?',
|
'确定要退出系统吗?',
|
||||||
@ -33,23 +129,12 @@ const handleLogout = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
// 1. 调用 Store 的 logout 清除状态
|
|
||||||
userStore.logout()
|
userStore.logout()
|
||||||
|
ElMessage({ type: 'success', message: '已安全退出' })
|
||||||
// 2. 提示消息
|
|
||||||
ElMessage({
|
|
||||||
type: 'success',
|
|
||||||
message: '已安全退出',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3. 强制跳转回登录页
|
|
||||||
await router.replace('/login')
|
await router.replace('/login')
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {})
|
||||||
// 取消操作
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// --- 退出登录逻辑 End ---
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -63,22 +148,24 @@ const handleLogout = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
<!-- 用户下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click" @command="(cmd: string) => { if (cmd === 'profile') openProfileDialog(); if (cmd === 'logout') handleLogout(); }">
|
||||||
<div class="user-profile">
|
<div class="user-profile">
|
||||||
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
|
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
|
||||||
<span class="user-name">{{ userStore.username || '管理员' }}</span>
|
<span class="user-name">{{ userStore.username || '管理员' }}</span>
|
||||||
|
<el-icon class="dropdown-arrow"><ArrowDown /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
<el-divider direction="vertical" />
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="profile">
|
||||||
<el-button
|
<el-icon><User /></el-icon> 个人中心
|
||||||
type="danger"
|
</el-dropdown-item>
|
||||||
link
|
<el-dropdown-item command="logout" divided>
|
||||||
@click="handleLogout"
|
<el-icon><SwitchButton /></el-icon> 退出登录
|
||||||
class="logout-btn"
|
</el-dropdown-item>
|
||||||
>
|
</el-dropdown-menu>
|
||||||
<el-icon style="margin-right: 4px; font-size: 16px"><SwitchButton /></el-icon>
|
</template>
|
||||||
退出
|
</el-dropdown>
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -89,9 +176,90 @@ const handleLogout = () => {
|
|||||||
<footer v-if="!isLoginPage" class="app-footer">
|
<footer v-if="!isLoginPage" class="app-footer">
|
||||||
<span class="version-tag">
|
<span class="version-tag">
|
||||||
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||||
当前版本:V3.3(3.19盘库修改)
|
当前版本:V3.4(3.23盘库修改最终章部署版)
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- ================================================================ -->
|
||||||
|
<!-- 个人中心 / 修改密码 弹窗 -->
|
||||||
|
<!-- 【严格脱敏】不展示系统角色字段 -->
|
||||||
|
<!-- ================================================================ -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="profileDialogVisible"
|
||||||
|
title="个人中心"
|
||||||
|
width="480px"
|
||||||
|
:close-on-click-modal="!passwordLoading"
|
||||||
|
destroy-on-close
|
||||||
|
class="profile-dialog"
|
||||||
|
>
|
||||||
|
<div v-loading="profileLoading">
|
||||||
|
<!-- 个人信息(只读) -->
|
||||||
|
<div class="profile-info-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><User /></el-icon> 个人信息
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="1" border class="profile-descriptions">
|
||||||
|
<el-descriptions-item label="姓名/账号">
|
||||||
|
<el-tag type="info" effect="plain">{{ profileForm.display_name || '-' }}</el-tag>
|
||||||
|
<span class="account-hint">(账号: {{ profileForm.username || '-' }})</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="所属部门">
|
||||||
|
<el-tag type="success" effect="light">{{ profileForm.department || '-' }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<!-- 【严格脱敏】系统角色字段已移除,不在此展示 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider>
|
||||||
|
<el-icon><Lock /></el-icon> 修改密码
|
||||||
|
</el-divider>
|
||||||
|
|
||||||
|
<!-- 修改密码表单(无需旧密码,JWT 已验证身份) -->
|
||||||
|
<el-form
|
||||||
|
ref="passwordFormRef"
|
||||||
|
:model="passwordForm"
|
||||||
|
label-width="110px"
|
||||||
|
class="password-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="新密码" prop="new_password">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.new_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码(至少6位)"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认新密码" prop="confirm_password">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.confirm_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="profileDialogVisible = false" :disabled="passwordLoading">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="passwordLoading"
|
||||||
|
@click="submitPasswordChange"
|
||||||
|
class="confirm-btn"
|
||||||
|
>
|
||||||
|
<el-icon v-if="!passwordLoading"><Lock /></el-icon>
|
||||||
|
确认修改
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -158,7 +326,14 @@ const handleLogout = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 {
|
.user-avatar {
|
||||||
@ -171,12 +346,10 @@ const handleLogout = () => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
.dropdown-arrow {
|
||||||
font-weight: 400;
|
font-size: 12px;
|
||||||
padding: 4px 8px;
|
color: #909399;
|
||||||
}
|
margin-left: 2px;
|
||||||
.logout-btn:hover {
|
|
||||||
color: #f56c6c !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
@ -204,4 +377,41 @@ const handleLogout = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -50,3 +50,20 @@ export function deleteUser(id: number) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 【新增】获取当前登录用户的个人资料(只含姓名/账号/部门,严格脱敏)
|
||||||
|
export function getMyProfile() {
|
||||||
|
return request({
|
||||||
|
url: '/v1/auth/me',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【新增】自我修改密码(无需旧密码,JWT 已证明身份)
|
||||||
|
export function changeMyPassword(data: { new_password: string; confirm_password: string }) {
|
||||||
|
return request({
|
||||||
|
url: '/v1/auth/me/password',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -36,6 +36,11 @@
|
|||||||
借库申请
|
借库申请
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码温馨提示 -->
|
||||||
|
<div class="password-tip">
|
||||||
|
💡 提示:为了您的账号安全,请点击右上角<strong>个人头像</strong>修改默认登录密码。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@ -415,6 +420,22 @@ const handleNav = (path: string) => {
|
|||||||
flex-wrap: wrap; /* 防止屏幕过窄时按钮挤压 */
|
flex-wrap: wrap; /* 防止屏幕过窄时按钮挤压 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 修改密码温馨提示 */
|
||||||
|
.password-tip {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-tip strong {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
/* 给按钮加一点悬浮效果 */
|
/* 给按钮加一点悬浮效果 */
|
||||||
.el-button {
|
.el-button {
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|||||||
@ -142,6 +142,7 @@
|
|||||||
highlight-current-row
|
highlight-current-row
|
||||||
header-cell-class-name="table-header-gray"
|
header-cell-class-name="table-header-gray"
|
||||||
@sort-change="handleSortChange"
|
@sort-change="handleSortChange"
|
||||||
|
:key="hasColumnPermission('sn_bn') ? 'sn' : 'nosn'"
|
||||||
>
|
>
|
||||||
<template v-for="col in allColumns" :key="col.prop">
|
<template v-for="col in allColumns" :key="col.prop">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@ -818,7 +819,7 @@ const displayData = computed(() => {
|
|||||||
const key = `${item.sku || ''}_${item.spec_model || item.spec || ''}`
|
const key = `${item.sku || ''}_${item.spec_model || item.spec || ''}`
|
||||||
if (aggMap.has(key)) {
|
if (aggMap.has(key)) {
|
||||||
const existing = aggMap.get(key)
|
const existing = aggMap.get(key)
|
||||||
// 累加库存数量
|
// 累加库存数量(原地修改已拷贝的对象,不影响原始数据)
|
||||||
existing.qty_stock = (existing.qty_stock || 0) + (item.qty_stock || 0)
|
existing.qty_stock = (existing.qty_stock || 0) + (item.qty_stock || 0)
|
||||||
existing.qty_available = (existing.qty_available || 0) + (item.qty_available || 0)
|
existing.qty_available = (existing.qty_available || 0) + (item.qty_available || 0)
|
||||||
existing.stock_quantity = (existing.stock_quantity || 0) + (item.stock_quantity || 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.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)
|
existing.unit_total_cost = (existing.unit_total_cost || 0) + (item.unit_total_cost || 0)
|
||||||
} else {
|
} else {
|
||||||
aggMap.set(key, { ...item })
|
// 【关键修复】使用 Object.assign 深拷贝第一条记录的所有字段,
|
||||||
|
// 绝不能只保留 sku 和 qty,否则其他列(公司/名称/规格等)会读不到数据
|
||||||
|
aggMap.set(key, Object.assign({}, item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(aggMap.values())
|
return Array.from(aggMap.values())
|
||||||
|
|||||||
Reference in New Issue
Block a user