refactor: API口径统一 — 统计函数/员工列表/前端服务层

后端:
- /dashboard/compliance-score 改用 getHealthCheck,与体检诊断同口径
- 新增 /employees/list 端点(不分页,供各页面下拉选择)
- /employees/all-lite 增加 position 字段和 status 筛选参数
- getHealthCheck 未签合同改为查无合同记录的员工(非UNSIGNED类型)

前端:
- 新建 lib/api-services.ts 统一 API 服务层
- employeeApi/rosterApi/dashboardApi/attendanceApi 统一封装
- 6个页面迁移到统一 API 调用:
  - Attendance: employeeApi.list + rosterApi.departments
  - Termination: rosterApi.list + rosterApi.departments
  - Money: rosterApi.list + rosterApi.departments
  - PortalQRModal: employeeApi.list
  - AIAssistant: 3处 employeeApi.list 替代 /roster?pageSize=999
  - Contracts: rosterApi.departments
  - Dashboard: rosterApi.expiringContracts + dashboardApi.healthCheck/workforceStats
This commit is contained in:
selfrelease
2026-08-01 15:29:13 +08:00
parent 5dd92a0e20
commit fb924dea98
12 changed files with 445 additions and 62 deletions
+2 -2
View File
@@ -7,6 +7,7 @@ import { QRCodeSVG } from 'qrcode.react'
import { Smartphone, Copy, Check, Search, Loader2, User, ChevronRight } from 'lucide-react'
import Modal from './ui/Modal'
import api from '../lib/api'
import { employeeApi } from '../lib/api-services'
interface Employee {
id: string
@@ -30,8 +31,7 @@ export default function PortalQRModal({ open, onClose }: { open: boolean; onClos
useEffect(() => {
if (!open) return
setLoading(true)
api.get('/roster', { params: { pageSize: 999 } }).then((res: any) => {
const list = res.data?.data || res.data || []
employeeApi.list({ status: 'ACTIVE' }).then((list: any) => {
setEmployees(list.map((e: any) => ({
id: e.id,
name: e.name,
+143
View File
@@ -0,0 +1,143 @@
/**
* 统一 API 服务层
* 所有页面应通过此模块调用 API,确保口径统一
*/
import api from './api'
/** axios 拦截器已返回 response.data,但 TS 类型仍是 AxiosResponse,用 as any 绕过 */
const get = ((url: string, config?: any) => api.get(url, config)) as any
const post = ((url: string, data?: any, config?: any) => api.post(url, data, config)) as any
const put = ((url: string, data?: any, config?: any) => api.put(url, data, config)) as any
/** 统一响应解析:返回一个回调函数,用于 .then() 中取 data 字段 */
function unwrap<T>(): (res: any) => T {
return (res: any) => (res?.data ?? res) as T
}
// ========== 员工相关 ==========
export interface EmployeeOption {
id: string
name: string
department?: string
position?: string
phone?: string
gender?: string
status?: string
}
export const employeeApi = {
/** 标准员工列表(不分页,供下拉选择等) */
list: (params?: { status?: string; department?: string }) =>
get('/employees/list', { params }).then(unwrap<EmployeeOption[]>()),
/** 轻量级全量员工列表(含 RESIGNED) */
allLite: (params?: { status?: string; department?: string }) =>
get('/employees/all-lite', { params }).then(unwrap<EmployeeOption[]>()),
/** 员工详情 */
detail: (id: string) =>
get(`/employees/${id}`).then(unwrap<any>()),
/** 员工档案(花名册) */
profile: (id: string) =>
get(`/roster/${id}/profile`).then(unwrap<any>()),
}
// ========== 花名册相关 ==========
export interface RosterParams {
page?: number
pageSize?: number
search?: string
status?: string
department?: string
contractStatus?: string
}
export interface RosterResponse {
data: any[]
pagination: { page: number; pageSize: number; total: number; totalPages: number }
globalRiskStats?: { expiring: number; expired: number; unsigned: number }
}
export const rosterApi = {
/** 花名册分页列表 */
list: (params: RosterParams) =>
get('/roster', { params }) as Promise<RosterResponse>,
/** 部门列表 */
departments: () =>
get('/roster/departments').then(unwrap<string[]>()),
/** 合同类型 */
contractTypes: () =>
get('/roster/contract-types').then(unwrap<any[]>()),
/** 即将到期合同 */
expiringContracts: () =>
get('/roster/contracts/expiring').then(unwrap<any[]>()),
/** 违纪记录 */
disciplinary: (employeeId: string) =>
get(`/roster/${employeeId}/disciplinary`).then(unwrap<any[]>()),
/** 证据链 */
evidenceChain: (employeeId: string) =>
get(`/roster/${employeeId}/evidence-chain`).then(unwrap<any>()),
}
// ========== 仪表盘相关 ==========
export const dashboardApi = {
/** 仪表盘主数据 */
data: () =>
get('/dashboard').then(unwrap<any>()),
/** 用工体检诊断(6 维度,与评分标准一致) */
healthCheck: () =>
get('/dashboard/health-check').then(unwrap<any>()),
/** 体检诊断历史 */
healthCheckHistory: () =>
get('/dashboard/health-check/history').then(unwrap<any[]>()),
/** 风险中心(与工作台同口径) */
risks: () =>
get('/dashboard/risks').then(unwrap<any[]>()),
/** 月度日历 */
calendar: (month: string) =>
get(`/dashboard/calendar?month=${month}`).then(unwrap<any>()),
/** 成本分析 */
costAnalysis: (month: string) =>
get(`/dashboard/cost-analysis?month=${month}`).then(unwrap<any>()),
/** 人力信息总览 */
workforceStats: () =>
get('/dashboard/workforce-stats').then(unwrap<any>()),
/** 下一步行动 */
nextActions: () =>
get('/dashboard/workspace/next-actions').then(unwrap<any>()),
/** 入离职统计 */
turnoverStats: (months = 12) =>
get(`/dashboard/turnover-stats?months=${months}`).then(unwrap<any>()),
/** 绩效统计 */
performanceStats: (period: string) =>
get(`/dashboard/performance-stats?period=${period}`).then(unwrap<any>()),
/** 年度价值报告 */
annualValue: (year: number) =>
get(`/dashboard/annual-value?year=${year}`).then(unwrap<any>()),
/** 年度价值报告历史 */
annualValueHistory: () =>
get('/dashboard/annual-value/history').then(unwrap<any[]>()),
}
// ========== 考勤相关 ==========
export const attendanceApi = {
list: (params: { month: string; department?: string; status?: string }) =>
get('/attendance', { params }).then(unwrap<any>()),
stats: (month: string) =>
get(`/attendance/stats?month=${month}`).then(unwrap<any>()),
shifts: () =>
get('/attendance/shifts').then(unwrap<any[]>()),
shiftAssignments: (date: string) =>
get(`/attendance/shift-assignments?date=${date}`).then(unwrap<any>()),
daily: (date: string) =>
get(`/attendance/daily?date=${date}`).then(unwrap<any>()),
monthlyReport: (month: string) =>
get(`/attendance/monthly-report?month=${month}`).then(unwrap<any>()),
leaves: () =>
get('/attendance/leaves').then(unwrap<any[]>()),
publishRecords: () =>
get('/attendance/publish-records').then(unwrap<any[]>()),
}
+7 -15
View File
@@ -8,6 +8,7 @@ import rehypeRaw from 'rehype-raw'
import { Document, Packer, Paragraph, HeadingLevel, TextRun, Table, TableRow, TableCell, WidthType, BorderStyle, AlignmentType } from 'docx'
import { saveAs } from 'file-saver'
import api from '../lib/api'
import { employeeApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -633,11 +634,8 @@ function PredictTab() {
const [disciplinarySummary, setDisciplinarySummary] = useState('')
const { data: employees } = useQuery<any[]>({
queryKey: ['roster-list'],
queryFn: async () => {
const res = await api.get('/roster?pageSize=999') as any
return res.data?.items || res.data || []
},
queryKey: ['employee-list'],
queryFn: () => employeeApi.list({ status: 'ACTIVE' }),
})
const departments = [...new Set((employees || []).map((e: any) => e.department).filter(Boolean))]
@@ -1384,11 +1382,8 @@ function ReviewTab() {
}
const { data: employees } = useQuery<any[]>({
queryKey: ['roster-list'],
queryFn: async () => {
const res = await api.get('/roster') as any
return res.data?.items || res.data || []
},
queryKey: ['employee-list'],
queryFn: () => employeeApi.list({ status: 'ACTIVE' }),
})
const handleReview = async () => {
@@ -1580,11 +1575,8 @@ function CaseTab() {
const { history, saveMutation, deleteMutation, loadHistory } = useAIHistory('case')
const { data: employees } = useQuery<any[]>({
queryKey: ['roster-list'],
queryFn: async () => {
const res = await api.get('/roster') as any
return res.data?.items || res.data || []
},
queryKey: ['employee-list'],
queryFn: () => employeeApi.list({ status: 'ACTIVE' }),
})
const handleMatch = async () => {
+4 -9
View File
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { CalendarCheck, CheckCircle, Clock, AlertCircle, Plus, Trash2, Calendar, Users, BarChart3, Plane, Upload, Download, X, Send, Loader2, CheckCheck } from 'lucide-react'
import api from '../lib/api'
import { employeeApi, rosterApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -124,10 +125,7 @@ function ConfirmTab() {
const { data: departmentList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
const res = await api.get('/roster/departments') as any
return res.data || []
},
queryFn: () => rosterApi.departments(),
})
const { data: publishRecords } = useQuery<any[]>({
@@ -982,11 +980,8 @@ function LeavesTab() {
})
const { data: rosterData } = useQuery<any>({
queryKey: ['roster-employees'],
queryFn: async () => {
const res = await api.get('/roster?pageSize=200') as any
return res.data
},
queryKey: ['employee-list'],
queryFn: () => employeeApi.list({ status: 'ACTIVE' }),
})
const createMutation = useMutation({
+1
View File
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Calculator, Info, AlertCircle } from 'lucide-react'
import api from '../lib/api'
import { rosterApi } from '../lib/api-services'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import { Input, Label, Select } from '../components/ui/Input'
+2 -4
View File
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Plus, Search, Paperclip, Trash2, X, FileText, Download, Eye } from 'lucide-react'
import { toast } from 'sonner'
import api from '../lib/api'
import { rosterApi } from '../lib/api-services'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import { Input, Label, Select } from '../components/ui/Input'
@@ -66,10 +67,7 @@ export default function Contracts() {
const { data: departmentList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
const res = await api.get('/roster/departments') as any
return res.data || []
},
queryFn: () => rosterApi.departments(),
})
return (
+4 -12
View File
@@ -5,6 +5,7 @@ import { Link } from 'react-router-dom'
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts'
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight, TrendingDown, ShieldCheck, Lightbulb, BookOpen, Sparkles, Repeat, XCircle, Loader2 } from 'lucide-react'
import api from '../lib/api'
import { dashboardApi, rosterApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -57,10 +58,7 @@ export default function Dashboard() {
const { data: expiringContracts } = useQuery<any>({
queryKey: ['expiring-contracts'],
queryFn: async () => {
const res = await api.get('/roster/contracts/expiring') as any
return res.data
},
queryFn: () => rosterApi.expiringContracts(),
})
const currentMonth = new Date().toISOString().slice(0, 7)
@@ -75,18 +73,12 @@ export default function Dashboard() {
const { data: complianceScore } = useQuery<any>({
queryKey: ['compliance-score'],
queryFn: async () => {
const res = await api.get('/dashboard/compliance-score') as any
return res.data
},
queryFn: () => dashboardApi.healthCheck(),
})
const { data: workforceStats } = useQuery<any>({
queryKey: ['workforce-stats'],
queryFn: async () => {
const res = await api.get('/dashboard/workforce-stats') as any
return res.data
},
queryFn: () => dashboardApi.workforceStats(),
})
const resolveMutation = useMutation({
+3 -9
View File
@@ -7,6 +7,7 @@ import { Calculator, AlertCircle, Info, Check, Upload, Layers, Settings as Setti
import { Stepper, type Step } from '../components/ui/Stepper'
import { InlineAlert } from '../components/ui/InlineAlert'
import api from '../lib/api'
import { rosterApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -72,21 +73,14 @@ function CustomEmployeeSelector({ selectedIds, onChange }: { selectedIds: string
const { data: employees } = useQuery<any>({
queryKey: ['roster-for-batch', search, filterDept],
queryFn: async () => {
const params: any = { pageSize: 999 }
if (search) params.search = search
if (filterDept) params.department = filterDept
params.status = 'ACTIVE'
const res = await api.get('/roster', { params }) as any
const res = await rosterApi.list({ pageSize: 999, search, department: filterDept, status: 'ACTIVE' })
return res.data || []
},
})
const { data: deptList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
const res = await api.get('/roster/departments') as any
return res.data || []
},
queryFn: () => rosterApi.departments(),
})
const toggle = (id: string) => {
+4 -6
View File
@@ -6,6 +6,7 @@ import { Stepper } from '../components/ui/Stepper'
import { InlineAlert } from '../components/ui/InlineAlert'
import jsPDF from 'jspdf'
import api from '../lib/api'
import { rosterApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -217,10 +218,10 @@ export default function Termination() {
const [draftPage, setDraftPage] = useState(1)
const [draftPageSize, setDraftPageSize] = useState(20)
const { data: employees } = useQuery<RosterEmployee[]>({
const { data: employees } = useQuery<any[]>({
queryKey: ['roster-for-termination'],
queryFn: async () => {
const res = await api.get('/roster') as any
const res = await rosterApi.list({ pageSize: 999 })
return res.data
},
})
@@ -229,10 +230,7 @@ export default function Termination() {
const { data: departmentList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
const res = await api.get('/roster/departments') as any
return res.data || []
},
queryFn: () => rosterApi.departments(),
})
const { data: profile } = useQuery<EmployeeProfile>({