feat: fix table alignment in product view and implement self-service password update with role masking

This commit is contained in:
DXC
2026-03-23 11:41:09 +08:00
parent f701ed7fc8
commit ec5331ffb3
4 changed files with 393 additions and 44 deletions

View File

@ -1,15 +1,16 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
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 { getMyProfile, changeMyPassword } from '@/api/auth'
const router = useRouter()
const route = useRoute() // [新增] 获取当前路由对象
const route = useRoute()
const userStore = useUserStore()
// [新增] 计算属性:判断当前是否是登录页
// 计算属性:判断当前是否是登录页
const isLoginPage = computed(() => {
return route.path === '/login'
})
@ -21,7 +22,105 @@ 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({
old_password: '',
new_password: '',
confirm_password: ''
})
const passwordFormRef = ref()
// 打开个人中心弹窗
const openProfileDialog = async () => {
profileDialogVisible.value = true
profileLoading.value = true
passwordForm.value = { old_password: '', new_password: '', confirm_password: '' }
try {
const res: any = await getMyProfile()
const data = res.data || res
if (data && data.data) {
profileForm.value = data.data
}
} catch (e: any) {
ElMessage.error(e?.message || '获取个人资料失败')
} finally {
profileLoading.value = false
}
}
// 提交修改密码
const submitPasswordChange = async () => {
const { old_password, new_password, confirm_password } = passwordForm.value
if (!old_password || !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({
old_password,
new_password,
confirm_password
})
const msg = res?.data?.msg || res?.msg || '修改成功'
ElMessage.success(msg)
// 如果是普通用户修改成功,提示重新登录
if (msg.includes('重新登录')) {
profileDialogVisible.value = false
setTimeout(() => {
userStore.logout()
router.replace('/login')
}, 1500)
} else {
// 超级管理员等特殊提示,直接清空表单
passwordForm.value = { old_password: '', new_password: '', confirm_password: '' }
}
} catch (e: any) {
const errMsg = e?.response?.data?.msg || e?.message || '修改失败'
ElMessage.error(errMsg)
} finally {
passwordLoading.value = false
}
}
// ================================================================
// 退出登录
// ================================================================
const handleLogout = () => {
ElMessageBox.confirm(
'确定要退出系统吗?',
@ -33,23 +132,12 @@ const handleLogout = () => {
}
)
.then(async () => {
// 1. 调用 Store 的 logout 清除状态
userStore.logout()
// 2. 提示消息
ElMessage({
type: 'success',
message: '已安全退出',
})
// 3. 强制跳转回登录页
ElMessage({ type: 'success', message: '已安全退出' })
await router.replace('/login')
})
.catch(() => {
// 取消操作
})
.catch(() => {})
}
// --- 退出登录逻辑 End ---
</script>
<template>
@ -63,22 +151,24 @@ const handleLogout = () => {
</div>
<div class="header-right">
<div class="user-profile">
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
<span class="user-name">{{ userStore.username || '管理员' }}</span>
</div>
<el-divider direction="vertical" />
<el-button
type="danger"
link
@click="handleLogout"
class="logout-btn"
>
<el-icon style="margin-right: 4px; font-size: 16px"><SwitchButton /></el-icon>
退出
</el-button>
<!-- 用户下拉菜单 -->
<el-dropdown trigger="click" @command="(cmd: string) => { if (cmd === 'profile') openProfileDialog(); if (cmd === 'logout') handleLogout(); }">
<div class="user-profile">
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
<span class="user-name">{{ userStore.username || '管理员' }}</span>
<el-icon class="dropdown-arrow"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon> 个人中心
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<el-icon><SwitchButton /></el-icon> 退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
@ -89,9 +179,100 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.3(3.19盘库修改
当前版本:V3.4(3.23盘库修改最终章部署版
</span>
</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>
<!-- 修改密码表单 -->
<el-form
ref="passwordFormRef"
:model="passwordForm"
label-width="110px"
class="password-form"
>
<el-form-item label="旧密码" prop="old_password">
<el-input
v-model="passwordForm.old_password"
type="password"
placeholder="请输入当前密码"
show-password
clearable
/>
</el-form-item>
<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>
</template>
@ -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;
}
</style>

View File

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

View File

@ -142,6 +142,7 @@
highlight-current-row
header-cell-class-name="table-header-gray"
@sort-change="handleSortChange"
:key="hasColumnPermission('sn_bn') ? 'sn' : 'nosn'"
>
<template v-for="col in allColumns" :key="col.prop">
<el-table-column
@ -818,7 +819,7 @@ const displayData = computed(() => {
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())