({
+ mutationFn: async (vars: { city: string; effectiveFrom: string; type: 'social' | 'housing' }) => {
+ const res = await api.post('/social/ai-suggest', vars) as any
+ return res.data
+ },
+ onSuccess: (data) => {
+ if (isHousing) {
+ activeSetNewVersion({
+ ...activeNewVersion,
+ baseMin: data.baseMin ?? activeNewVersion.baseMin,
+ baseMax: data.baseMax ?? activeNewVersion.baseMax,
+ housingOrg: data.housingOrg ?? activeNewVersion.housingOrg,
+ housingEmp: data.housingEmp ?? activeNewVersion.housingEmp,
+ })
+ } else {
+ activeSetNewVersion({
+ ...activeNewVersion,
+ baseMin: data.baseMin ?? activeNewVersion.baseMin,
+ baseMax: data.baseMax ?? activeNewVersion.baseMax,
+ medicalBaseMin: data.medicalBaseMin ?? 0,
+ medicalBaseMax: data.medicalBaseMax ?? 0,
+ pensionOrg: data.pensionOrg ?? activeNewVersion.pensionOrg,
+ pensionEmp: data.pensionEmp ?? activeNewVersion.pensionEmp,
+ medicalOrg: data.medicalOrg ?? activeNewVersion.medicalOrg,
+ medicalEmp: data.medicalEmp ?? activeNewVersion.medicalEmp,
+ unemploymentOrg: data.unemploymentOrg ?? activeNewVersion.unemploymentOrg,
+ unemploymentEmp: data.unemploymentEmp ?? activeNewVersion.unemploymentEmp,
+ injuryOrg: data.injuryOrg ?? activeNewVersion.injuryOrg,
+ maternityOrg: data.maternityOrg ?? activeNewVersion.maternityOrg,
+ extraInsurances: data.extraInsurances ?? [],
+ })
+ }
+ toast.success('AI建议已填入,请核对后保存')
+ },
+ onError: () => {
+ toast.error('AI建议获取失败,请手动填写')
+ },
+ })
+
const previewAdjustMutation = useMutation({
mutationFn: async () => {
const res = await api.get(`/social/config/${config?.id}/adjust-preview`) as any
@@ -337,16 +378,52 @@ export default function SocialInsurance() {
{(tab === 'social' || tab === 'housing') && (
-
setCity(e.target.value)}
- placeholder="输入或选择城市"
- />
-
+ {showAddCity ? (
+
+ setNewCityName(e.target.value)}
+ placeholder="城市名"
+ autoFocus
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && newCityName.trim()) {
+ setCity(newCityName.trim())
+ setNewCityName('')
+ setShowAddCity(false)
+ queryClient.invalidateQueries({ queryKey: ['social-config-cities'] })
+ }
+ if (e.key === 'Escape') { setShowAddCity(false); setNewCityName('') }
+ }}
+ />
+
+
+
+ ) : (
+
+
+
+
+ )}
)}
@@ -415,6 +492,17 @@ export default function SocialInsurance() {
失业(企业/个人){activeConfig.unemploymentOrg}% / {activeConfig.unemploymentEmp}%
工伤(企业){activeConfig.injuryOrg}%
生育(企业){activeConfig.maternityOrg}%
+ {Array.isArray(activeConfig.extraInsurances) && activeConfig.extraInsurances.map((ins: any, idx: number) => (
+
+ {ins.name}(企业/个人)
+
+ {ins.baseType === 'fixed'
+ ? `¥${ins.fixedAmount} / ¥${ins.empFixedAmount || 0}`
+ : `${ins.orgRate}% / ${ins.empRate}%`
+ }
+
+
+ ))}
)}
@@ -573,9 +661,22 @@ export default function SocialInsurance() {
新版本生效后,当前版本将自动归档。发薪批次计算时按批次月份匹配对应版本的配置。通常每年7月调基时新建版本。
+
diff --git a/frontend/src/pages/roster/AttachmentInfo.tsx b/frontend/src/pages/roster/AttachmentInfo.tsx
index dcea307..d4e3c92 100644
--- a/frontend/src/pages/roster/AttachmentInfo.tsx
+++ b/frontend/src/pages/roster/AttachmentInfo.tsx
@@ -29,11 +29,10 @@ export default function AttachmentInfo({ employeeId, attachments }: { employeeId
if (!file) return
// 文件类型校验
- const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/heic']
- const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic']
+ const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
- if (!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) {
- toast.error('不支持的文件格式,请上传 PDF、JPG、PNG 或 HEIC 格式')
+ if (!allowedExts.includes(ext)) {
+ toast.error('不支持的文件格式,支持 PDF、图片、Word、Excel 等常见格式')
return
}
diff --git a/frontend/src/pages/roster/BasicInfo.tsx b/frontend/src/pages/roster/BasicInfo.tsx
index 8cc6a9e..ac2275f 100644
--- a/frontend/src/pages/roster/BasicInfo.tsx
+++ b/frontend/src/pages/roster/BasicInfo.tsx
@@ -30,11 +30,10 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
const handleFileUpload = (e: React.ChangeEvent) => {
const file = e.target.files?.[0]
if (!file) return
- const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/heic']
- const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic']
+ const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
- if (!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) {
- toast.error('不支持的文件格式,请上传 PDF、JPG、PNG 或 HEIC 格式')
+ if (!allowedExts.includes(ext)) {
+ toast.error('不支持的文件格式,支持 PDF、图片、Word、Excel 等常见格式')
return
}
if (file.size > 10 * 1024 * 1024) {
diff --git a/frontend/src/pages/roster/ContractInfo.tsx b/frontend/src/pages/roster/ContractInfo.tsx
index 1d325d5..1389d67 100644
--- a/frontend/src/pages/roster/ContractInfo.tsx
+++ b/frontend/src/pages/roster/ContractInfo.tsx
@@ -12,40 +12,48 @@ import { fmt } from "./shared"
export default function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string; contracts: any[]; hireDate: string }) {
const queryClient = useQueryClient()
const [showForm, setShowForm] = useState(false)
- const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', electronicContractNo: '', electronicContractUrl: '' })
+ const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', attachments: [] as { name: string; url: string }[], electronicContractNo: '', electronicContractUrl: '' })
const contractFileRef = useRef(null)
+ const [previewUrl, setPreviewUrl] = useState(null)
const addContractMutation = useMutation({
mutationFn: (data: any) => api.post('/employees/contracts', { ...data, employeeId }),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) },
})
+ const deleteContractMutation = useMutation({
+ mutationFn: (contractId: string) => api.delete(`/employees/contracts/${contractId}`),
+ onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); toast.success('合同已删除') },
+ })
+
const handleContractFileUpload = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0]
- if (!file) return
+ const files = e.target.files
+ if (!files || files.length === 0) return
- // 文件类型校验
- const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/heic']
- const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic']
- const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
- if (!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) {
- toast.error('不支持的文件格式,请上传 PDF、JPG、PNG 或 HEIC 格式')
- return
- }
-
- // 文件大小校验(10MB)
+ const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
const maxSize = 10 * 1024 * 1024
- if (file.size > maxSize) {
- const formatSize = (bytes: number) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)}KB` : `${(bytes / 1024 / 1024).toFixed(1)}MB`
- toast.error(`文件过大,请上传小于 10MB 的文件(当前: ${formatSize(file.size)})`)
- return
- }
- const reader = new FileReader()
- reader.onload = (event) => {
- setForm({ ...form, attachmentUrl: event.target?.result as string })
+ for (const file of Array.from(files)) {
+ const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
+ if (!allowedExts.includes(ext)) {
+ toast.error(`不支持的文件格式: ${file.name}`)
+ continue
+ }
+ if (file.size > maxSize) {
+ toast.error(`文件过大: ${file.name}(最大 10MB)`)
+ continue
+ }
+ const reader = new FileReader()
+ reader.onload = (event) => {
+ setForm(prev => ({
+ ...prev,
+ attachments: [...prev.attachments, { name: file.name, url: event.target?.result as string }],
+ }))
+ }
+ reader.readAsDataURL(file)
}
- reader.readAsDataURL(file)
+ // 清空 input 以便重复选择同一文件
+ e.target.value = ''
}
const typeMap: Record = { FIXED: '固定期限', UNFIXED: '无固定期限', UNSIGNED: '未签订' }
@@ -102,6 +110,7 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
probationSalary: 0,
signMethod: 'PAPER',
attachmentUrl: '',
+ attachments: [],
electronicContractNo: '',
electronicContractUrl: '',
})
@@ -158,14 +167,29 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
{form.signMethod === 'PAPER' && (
-
-
+
+
- {form.attachmentUrl &&
✓ 已上传}
+ {form.attachments.length > 0 &&
{form.attachments.length} 个文件}
+ {form.attachments.length > 0 && (
+
+ {form.attachments.map((att, idx) => (
+
+
+
+
+ ))}
+
+ )}
+
支持 PDF、JPG、PNG、HEIC、GIF、BMP、WebP、TIFF、Word、Excel 等格式,每个文件最大 10MB
)}
{form.signMethod === 'ELECTRONIC' && (
@@ -178,6 +202,7 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl