feat: 员工端密码登录完整支持

1. createEmployee 创建员工时自动设置默认密码(手机号后6位)
2. 管理员重置密码接口 POST /employees/:id/reset-password
   (重置为手机号后6位)
3. 员工自己修改密码接口 POST /portal/change-password
   (需验证旧密码,新密码至少6位)
4. 花名册详情添加"重置密码"按钮,显示默认密码提示
5. 员工端导航栏添加"修改密码"入口,弹窗修改密码

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-16 11:28:56 +08:00
parent 26d0b7070d
commit d7fdec49fa
6 changed files with 201 additions and 33 deletions
+6
View File
@@ -81,6 +81,9 @@ export const employeeApi = {
/** 重新入职 */
rehire: (id: string, data: Record<string, unknown>) =>
post(`/employees/${id}/rehire`, data),
/** 重置员工密码(管理员,重置为手机号后6位) */
resetPassword: (id: string) =>
post(`/employees/${id}/reset-password`).then(unwrap<any>()),
/** 添加合同 */
addContract: (data: Record<string, unknown>) =>
post('/employees/contracts', data),
@@ -1026,6 +1029,9 @@ export const portalApi = {
/** 自动登录 */
autoLogin: (token: string) =>
portalGet('/auto-login', { params: { token } }).then(unwrap<any>()),
/** 修改密码(员工自己) */
changePassword: (oldPassword: string, newPassword: string) =>
portalPost('/change-password', { oldPassword, newPassword }).then(unwrap<any>()),
/** 生成自动登录令牌(管理端) */
generateAutoLoginToken: (employeeId: string) =>
post('/portal/auto-login-token', { employeeId }).then(unwrap<any>()),
+100 -25
View File
@@ -2,8 +2,14 @@
* 员工端底部导航栏
*/
import { useState } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { DollarSign, FileText, ScrollText, LogOut, PenTool, ClipboardList } from 'lucide-react'
import { DollarSign, FileText, ScrollText, LogOut, PenTool, ClipboardList, KeyRound } from 'lucide-react'
import { toast } from 'sonner'
import { portalApi } from '../../lib/api-services'
import Modal from '../../components/ui/Modal'
import Button from '../../components/ui/Button'
import { Input, Label } from '../../components/ui/Input'
const navItems = [
{ path: '/portal/payslip', label: '工资条', icon: DollarSign },
@@ -16,6 +22,11 @@ const navItems = [
export default function PortalNav() {
const location = useLocation()
const navigate = useNavigate()
const [showPwdModal, setShowPwdModal] = useState(false)
const [oldPwd, setOldPwd] = useState('')
const [newPwd, setNewPwd] = useState('')
const [confirmPwd, setConfirmPwd] = useState('')
const [pwdLoading, setPwdLoading] = useState(false)
const handleLogout = () => {
localStorage.removeItem('portalToken')
@@ -23,31 +34,95 @@ export default function PortalNav() {
navigate('/portal/login')
}
/** 修改密码 */
const handleChangePassword = async () => {
if (!oldPwd || !newPwd || !confirmPwd) {
toast.error('请填写所有字段')
return
}
if (newPwd.length < 6) {
toast.error('新密码至少6位')
return
}
if (newPwd !== confirmPwd) {
toast.error('两次输入的新密码不一致')
return
}
setPwdLoading(true)
try {
const data: any = await portalApi.changePassword(oldPwd, newPwd)
toast.success(data?.message || '密码修改成功')
setShowPwdModal(false)
setOldPwd(''); setNewPwd(''); setConfirmPwd('')
} catch (err: any) {
toast.error(err?.response?.data?.error?.message || '修改失败')
} finally {
setPwdLoading(false)
}
}
return (
<div className="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
<div className="flex items-center gap-4">
{navItems.map((item) => {
const Icon = item.icon
const active = location.pathname === item.path
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-1 text-sm ${active ? 'text-primary font-medium' : 'text-gray-500 hover:text-gray-700'}`}
>
<Icon className="w-4 h-4" />
{item.label}
</Link>
)
})}
<>
<div className="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
<div className="flex items-center gap-4">
{navItems.map((item) => {
const Icon = item.icon
const active = location.pathname === item.path
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-1 text-sm ${active ? 'text-primary font-medium' : 'text-gray-500 hover:text-gray-700'}`}
>
<Icon className="w-4 h-4" />
{item.label}
</Link>
)
})}
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowPwdModal(true)}
className="flex items-center gap-1 text-sm text-gray-400 hover:text-gray-600"
>
<KeyRound className="w-4 h-4" />
</button>
<button
onClick={handleLogout}
className="flex items-center gap-1 text-sm text-gray-400 hover:text-gray-600"
>
<LogOut className="w-4 h-4" />
退
</button>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-1 text-sm text-gray-400 hover:text-gray-600"
>
<LogOut className="w-4 h-4" />
退
</button>
</div>
{/* 修改密码弹窗 */}
{showPwdModal && (
<Modal open onClose={() => setShowPwdModal(false)} title="修改密码">
<div className="space-y-3">
<div>
<Label></Label>
<Input type="password" value={oldPwd} onChange={(e) => setOldPwd(e.target.value)} placeholder="请输入旧密码" />
</div>
<div>
<Label></Label>
<Input type="password" value={newPwd} onChange={(e) => setNewPwd(e.target.value)} placeholder="至少6位" />
</div>
<div>
<Label></Label>
<Input type="password" value={confirmPwd} onChange={(e) => setConfirmPwd(e.target.value)} placeholder="再次输入新密码" />
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" onClick={() => setShowPwdModal(false)}></Button>
<Button onClick={handleChangePassword} disabled={pwdLoading}>
{pwdLoading ? '修改中...' : '确认修改'}
</Button>
</div>
</div>
</Modal>
)}
</>
)
}
+33 -8
View File
@@ -9,7 +9,7 @@ import api from '../../lib/api'
import Card from "../../components/ui/Card"
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import { AlertTriangle, Paperclip, Trash2, Eye, Download, Copy } from "lucide-react"
import { AlertTriangle, Paperclip, Trash2, Eye, Download, Copy, KeyRound } from "lucide-react"
import { fmt } from "./shared"
export default function BasicInfo({ profile, employeeId, attachments }: { profile: any; employeeId: string; attachments: any[] }) {
@@ -29,6 +29,13 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }),
})
/** 重置员工密码(重置为手机号后6位) */
const resetPasswordMutation = useMutation({
mutationFn: () => employeeApi.resetPassword(employeeId),
onSuccess: (data: any) => toast.success(data?.message || '密码已重置'),
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '重置失败'),
})
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
@@ -493,12 +500,29 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
<div className="mt-4 pt-4 border-t">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-medium text-gray-600"></h3>
<Button size="sm" variant="secondary" onClick={() => {
const url = `${window.location.origin}/portal/login`
navigator.clipboard?.writeText(url)
}}>
</Button>
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={() => {
const url = `${window.location.origin}/portal/login`
navigator.clipboard?.writeText(url)
}}>
</Button>
<Button
size="sm"
variant="secondary"
onClick={async () => {
const ok = await confirm({
title: '重置员工密码',
message: `将重置「${profile.name}」的员工端密码为手机号后6位(${profile.phone?.slice(-6)}),确认操作?`,
})
if (ok) resetPasswordMutation.mutate()
}}
disabled={resetPasswordMutation.isPending}
>
<KeyRound className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
</div>
<div className="flex items-center gap-4">
<div className="bg-white p-3 rounded-lg border">
@@ -509,7 +533,8 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
/>
</div>
<div className="text-xs text-gray-500 space-y-1">
<p>使</p>
<p>使+</p>
<p>6{profile.phone?.slice(-6)}</p>
<p></p>
<p className="text-gray-400">{window.location.origin}/portal/login</p>
</div>