feat: 密码管理 — 用户自改密码 + 管理员重置密码
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m40s
Build and Deploy Web / build-and-deploy (push) Successful in 2m1s

- 侧边栏新增「修改密码」入口,任何用户可改自己的密码(需验证原密码)
- 用户管理编辑弹窗新增「重置密码」区域,管理员可直接重设任意用户密码
- 后端新增 POST /api/users/change-password 接口
- UserUpdate schema 增加 password 可选字段

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-02-25 15:51:30 +08:00
parent 11b1d9b105
commit 74106ac21b
5 changed files with 81 additions and 5 deletions

View File

@ -4,8 +4,8 @@ from sqlalchemy.orm import Session
from typing import List
from database import get_db
from models import User, Role, PhaseGroup
from schemas import UserCreate, UserUpdate, UserOut
from auth import get_current_user, hash_password, require_permission
from schemas import UserCreate, UserUpdate, UserOut, ChangePasswordRequest
from auth import get_current_user, hash_password, verify_password, require_permission
router = APIRouter(prefix="/api/users", tags=["用户管理"])
@ -86,11 +86,28 @@ def update_user(
user.social_insurance = req.social_insurance
if req.is_active is not None:
user.is_active = req.is_active
if req.password:
user.password_hash = hash_password(req.password)
db.commit()
db.refresh(user)
return user_to_out(user)
@router.post("/change-password")
def change_password(
req: ChangePasswordRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
if not verify_password(req.old_password, current_user.password_hash):
raise HTTPException(status_code=400, detail="原密码错误")
if len(req.new_password) < 4:
raise HTTPException(status_code=400, detail="新密码至少4位")
current_user.password_hash = hash_password(req.new_password)
db.commit()
return {"message": "密码修改成功"}
@router.get("/{user_id}", response_model=UserOut)
def get_user(
user_id: int,

View File

@ -37,6 +37,12 @@ class UserUpdate(BaseModel):
bonus: Optional[float] = None
social_insurance: Optional[float] = None
is_active: Optional[int] = None
password: Optional[str] = None # 管理员重置密码
class ChangePasswordRequest(BaseModel):
old_password: str
new_password: str
class UserOut(BaseModel):

View File

@ -51,6 +51,7 @@ export const userApi = {
create: (data) => api.post('/users', data),
update: (id, data) => api.put(`/users/${id}`, data),
get: (id) => api.get(`/users/${id}`),
changePassword: (data) => api.post('/users/change-password', data),
}
// ── 项目 ──

View File

@ -35,6 +35,11 @@
<div class="user-role">{{ authStore.user?.role_name }}</div>
</div>
</div>
<div class="user-actions">
<el-button text size="small" class="change-pwd-btn" @click="showChangePwd = true">
<el-icon :size="14"><Lock /></el-icon> 修改密码
</el-button>
</div>
</div>
</el-aside>
@ -55,13 +60,34 @@
<router-view />
</el-main>
</el-container>
<!-- 修改密码弹窗 -->
<el-dialog v-model="showChangePwd" title="修改密码" width="400px" destroy-on-close>
<el-form :model="pwdForm" label-width="80px">
<el-form-item label="原密码">
<el-input v-model="pwdForm.old_password" type="password" show-password placeholder="请输入当前密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwdForm.new_password" type="password" show-password placeholder="至少4位" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="pwdForm.confirm" type="password" show-password placeholder="再次输入新密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showChangePwd = false">取消</el-button>
<el-button type="primary" @click="handleChangePwd">确认修改</el-button>
</template>
</el-dialog>
</el-container>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { userApi } from '../api'
import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
@ -106,6 +132,22 @@ function handleLogout() {
authStore.logout()
router.push('/login')
}
//
const showChangePwd = ref(false)
const pwdForm = reactive({ old_password: '', new_password: '', confirm: '' })
async function handleChangePwd() {
if (!pwdForm.old_password) { ElMessage.warning('请输入原密码'); return }
if (pwdForm.new_password.length < 4) { ElMessage.warning('新密码至少4位'); return }
if (pwdForm.new_password !== pwdForm.confirm) { ElMessage.warning('两次输入的新密码不一致'); return }
try {
await userApi.changePassword({ old_password: pwdForm.old_password, new_password: pwdForm.new_password })
ElMessage.success('密码修改成功')
showChangePwd.value = false
pwdForm.old_password = ''; pwdForm.new_password = ''; pwdForm.confirm = ''
} catch {}
}
</script>
<style scoped>
@ -195,6 +237,9 @@ function handleLogout() {
.user-meta { display: flex; flex-direction: column; }
.user-name { font-size: 13px; font-weight: 600; color: var(--text-primary); line-height: 1.3; }
.user-role { font-size: 11px; color: var(--text-secondary); }
.user-actions { margin-top: 8px; }
.change-pwd-btn { color: var(--text-secondary) !important; font-size: 12px !important; padding: 0 !important; gap: 4px; }
.change-pwd-btn:hover { color: var(--primary) !important; }
/* ── 顶栏 ── */
.main-container { background: var(--bg-page); }

View File

@ -80,6 +80,10 @@
<el-form-item v-if="editingId" label="状态" style="margin-top:16px">
<el-switch v-model="form.is_active" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="停用" />
</el-form-item>
<el-divider v-if="editingId" content-position="left">重置密码</el-divider>
<el-form-item v-if="editingId" label="新密码">
<el-input v-model="form.password" type="password" show-password placeholder="留空则不修改密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreate = false">取消</el-button>
@ -127,17 +131,20 @@ function editUser(u) {
name: u.name, phase_group: u.phase_group, role_id: u.role_id,
monthly_salary: u.monthly_salary, bonus: u.bonus || 0,
social_insurance: u.social_insurance || 0, is_active: u.is_active,
password: '',
})
showCreate.value = true
}
async function handleSave() {
if (editingId.value) {
await userApi.update(editingId.value, {
const data = {
name: form.name, phase_group: form.phase_group, role_id: form.role_id,
monthly_salary: form.monthly_salary, bonus: form.bonus,
social_insurance: form.social_insurance, is_active: form.is_active,
})
}
if (form.password) data.password = form.password
await userApi.update(editingId.value, data)
} else {
await userApi.create(form)
}