feat: 花名册操作联动电子签署 + 修复重复创建

## 修复
- POST /contracts 不再自动创建签署记录(由前端根据signMethod决定),避免与ContractInfo重复创建

## 新增 SignMethodChoice 通用组件
- 花名册操作成功后弹窗选择签署方式:电子签/线下手签/稍后处理
- 电子签:自动创建ESignRecord(PENDING),员工在员工端签署
- 线下手签:跳转签署页面,预填员工/场景/标题,上传扫描件登记
- 稍后处理:跳过,可后续在签署页面手动发起

## 花名册4项操作联动签署方式选择
- 重新入职:操作成功后弹窗(scene=CONTRACT,劳动合同)
- 合同续签:操作成功后弹窗(scene=CONTRACT,续签劳动合同)
- 薪酬变更:操作成功后弹窗(scene=POLICY,薪酬调整确认书)
- 调岗调动:操作成功后弹窗(scene=POLICY,调岗确认书)

## ESign页面支持URL参数
- 读取 ?action=paper-sign 自动打开线下手签登记Modal
- 预填employeeId/scene/documentTitle

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-15 16:45:53 +08:00
parent 434c63c6d3
commit 3da61d5a09
4 changed files with 279 additions and 27 deletions
+41 -7
View File
@@ -6,7 +6,8 @@
* - 签署详情(含证据链查看)
* - 取消签署
*/
import { useState, useRef } from 'react'
import { useState, useRef, useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { toast } from 'sonner'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { PenTool, Plus, RefreshCw, ExternalLink, FileText, AlertCircle, Shield, ChevronLeft, Clock, CheckCircle2, XCircle, Eye, Upload, FileCheck } from 'lucide-react'
@@ -47,6 +48,7 @@ export default function ESign() {
const [showCreate, setShowCreate] = useState(false)
const [showPaperSign, setShowPaperSign] = useState(false)
const [detailId, setDetailId] = useState<string | null>(null)
const [searchParams] = useSearchParams()
const [formData, setFormData] = useState({
employeeId: '',
scene: 'CONTRACT',
@@ -54,6 +56,26 @@ export default function ESign() {
remark: '',
})
// 从 URL 参数读取,自动打开线下手签登记
useEffect(() => {
const action = searchParams.get('action')
if (action === 'paper-sign') {
setShowPaperSign(true)
// 预填表单
const employeeId = searchParams.get('employeeId')
const scene = searchParams.get('scene') as string
const documentTitle = searchParams.get('documentTitle') as string
if (employeeId || scene || documentTitle) {
setFormData({
employeeId: employeeId || '',
scene: scene || 'CONTRACT',
documentTitle: documentTitle || '',
remark: '花名册操作跳转登记',
})
}
}
}, [searchParams])
const { data: records = [], isLoading } = useQuery<any[]>({
queryKey: ['esign-records', filterStatus, filterScene],
queryFn: async () => {
@@ -323,10 +345,15 @@ export default function ESign() {
{/* 线下手签登记 Modal */}
{showPaperSign && (
<PaperSignModal
onClose={() => setShowPaperSign(false)}
initialEmployeeId={formData.employeeId}
initialScene={formData.scene as any}
initialDocumentTitle={formData.documentTitle}
initialRemark={formData.remark}
onClose={() => { setShowPaperSign(false); setFormData({ employeeId: '', scene: 'CONTRACT', documentTitle: '', remark: '' }) }}
onSuccess={() => {
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
setShowPaperSign(false)
setFormData({ employeeId: '', scene: 'CONTRACT', documentTitle: '', remark: '' })
}}
/>
)}
@@ -335,17 +362,24 @@ export default function ESign() {
}
// ===== 线下手签登记 Modal =====
function PaperSignModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
function PaperSignModal({ onClose, onSuccess, initialEmployeeId, initialScene, initialDocumentTitle, initialRemark }: {
onClose: () => void
onSuccess: () => void
initialEmployeeId?: string
initialScene?: string
initialDocumentTitle?: string
initialRemark?: string
}) {
const fileInputRef = useRef<HTMLInputElement>(null)
const [form, setForm] = useState({
employeeId: '',
scene: 'CONTRACT',
documentTitle: '',
employeeId: initialEmployeeId || '',
scene: initialScene || 'CONTRACT',
documentTitle: initialDocumentTitle || '',
signedAt: new Date().toISOString().slice(0, 10),
signedLocation: '',
witnessName: '',
witnessPhone: '',
remark: '',
remark: initialRemark || '',
})
const [scanFiles, setScanFiles] = useState<Array<{ name: string; url: string }>>([])
const [uploading, setUploading] = useState(false)