feat: 实现20260730优化方案全部功能

- AI文件审查:.docx上传提取文本,支持多种文档类型
- 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程
- 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word
- 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面
- 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤
- 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作
- Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型
- 前后端编译验证全部通过
This commit is contained in:
freedakgmail
2026-07-30 10:21:22 +08:00
parent 38b8849332
commit 42e0c650a4
24 changed files with 3639 additions and 35 deletions
+62 -2
View File
@@ -1329,15 +1329,60 @@ function PredictTab() {
)
}
const REVIEW_DOC_TYPES = [
{ value: 'labor_contract', label: '劳动合同' },
{ value: 'rescission', label: '协商解除协议' },
{ value: 'labor_service', label: '劳务协议' },
{ value: 'internship', label: '实习协议' },
{ value: 'nda', label: '保密协议' },
{ value: 'other', label: '其他' },
]
function ReviewTab() {
const [contractText, setContractText] = useState('')
const [result, setResult] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [uploading, setUploading] = useState(false)
const [docType, setDocType] = useState('labor_contract')
const [fileName, setFileName] = useState('')
const [showSaveModal, setShowSaveModal] = useState(false)
const [saveEmployeeId, setSaveEmployeeId] = useState('')
const [showHistory, setShowHistory] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const { history, saveMutation, deleteMutation, loadHistory } = useAIHistory('review')
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const ext = file.name.toLowerCase().split('.').pop()
if (ext !== 'docx' && ext !== 'doc') {
toast.error('仅支持 .docx 格式文件')
return
}
if (file.size > 100 * 1024 * 1024) {
toast.error('文件大小不能超过 100MB')
return
}
setUploading(true)
try {
const formData = new FormData()
formData.append('file', file)
const res = await api.post('/ai/review/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
}) as any
if (res.data?.text) {
setContractText(res.data.text)
setFileName(file.name)
toast.success(`已提取文件内容(${res.data.text.length} 字)`)
}
} catch (err: any) {
toast.error(err?.response?.data?.error?.message || '文件上传失败')
} finally {
setUploading(false)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
const { data: employees } = useQuery<any[]>({
queryKey: ['roster-list'],
queryFn: async () => {
@@ -1410,8 +1455,23 @@ function ReviewTab() {
<HistoryBar history={history || []} onLoad={handleLoadHistory} onDelete={(id) => deleteMutation.mutate(id)} />
</div>
)}
<div className="mt-3">
<Label></Label>
<div className="mt-3 space-y-3">
{/* 文件上传区 */}
<div>
<Label></Label>
<div className="flex items-center gap-2">
<Select value={docType} onChange={(e) => setDocType(e.target.value)} className="w-40">
{REVIEW_DOC_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
<input ref={fileInputRef} type="file" accept=".docx,.doc" onChange={handleFileUpload} className="hidden" />
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
{uploading ? (<><Loader2 className="w-4 h-4 animate-spin mr-1" />...</>) : (<><FileText className="w-4 h-4 mr-1" /> .docx </>)}
</Button>
{fileName && <span className="text-xs text-gray-500 truncate max-w-[200px]">{fileName}</span>}
</div>
</div>
<Label></Label>
<textarea
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-xs min-h-[200px] resize-y"
placeholder="粘贴劳动合同文本..."