|
@@ -1,13 +1,80 @@
|
|
|
import React from 'react'
|
|
import React from 'react'
|
|
|
import { View, Text, ScrollView } from '@tarojs/components'
|
|
import { View, Text, ScrollView } from '@tarojs/components'
|
|
|
|
|
+import { useQuery } from '@tanstack/react-query'
|
|
|
import { YongrenTabBarLayout } from '@d8d/yongren-shared-ui/components/YongrenTabBarLayout'
|
|
import { YongrenTabBarLayout } from '@d8d/yongren-shared-ui/components/YongrenTabBarLayout'
|
|
|
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
|
|
import { Navbar } from '@d8d/mini-shared-ui-components/components/navbar'
|
|
|
|
|
+import { enterpriseStatisticsClient } from '../api/enterpriseStatisticsClient'
|
|
|
|
|
+import type {
|
|
|
|
|
+ DisabilityTypeDistributionResponse,
|
|
|
|
|
+ GenderDistributionResponse,
|
|
|
|
|
+ AgeDistributionResponse,
|
|
|
|
|
+ HouseholdDistributionResponse,
|
|
|
|
|
+ JobStatusDistributionResponse,
|
|
|
|
|
+ SalaryDistributionResponse
|
|
|
|
|
+} from '../api/types'
|
|
|
|
|
|
|
|
export interface StatisticsProps {
|
|
export interface StatisticsProps {
|
|
|
// 组件属性定义(目前为空)
|
|
// 组件属性定义(目前为空)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const Statistics: React.FC<StatisticsProps> = () => {
|
|
const Statistics: React.FC<StatisticsProps> = () => {
|
|
|
|
|
+ // 获取残疾类型分布数据
|
|
|
|
|
+ const { data: disabilityData, isLoading: isLoadingDisability } = useQuery({
|
|
|
|
|
+ queryKey: ['statistics', 'disability-type-distribution'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseStatisticsClient['disability-type-distribution'].$get()
|
|
|
|
|
+ return await response.json()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 获取性别分布数据
|
|
|
|
|
+ const { data: genderData, isLoading: isLoadingGender } = useQuery({
|
|
|
|
|
+ queryKey: ['statistics', 'gender-distribution'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseStatisticsClient['gender-distribution'].$get()
|
|
|
|
|
+ return await response.json()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 获取年龄分布数据
|
|
|
|
|
+ const { data: ageData, isLoading: isLoadingAge } = useQuery({
|
|
|
|
|
+ queryKey: ['statistics', 'age-distribution'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseStatisticsClient['age-distribution'].$get()
|
|
|
|
|
+ return await response.json()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 获取户籍分布数据
|
|
|
|
|
+ const { data: householdData, isLoading: isLoadingHousehold } = useQuery({
|
|
|
|
|
+ queryKey: ['statistics', 'household-distribution'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseStatisticsClient['household-distribution'].$get()
|
|
|
|
|
+ return await response.json()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 获取在职状态分布数据
|
|
|
|
|
+ const { data: jobStatusData, isLoading: isLoadingJobStatus } = useQuery({
|
|
|
|
|
+ queryKey: ['statistics', 'job-status-distribution'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseStatisticsClient['job-status-distribution'].$get()
|
|
|
|
|
+ return await response.json()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 获取薪资分布数据
|
|
|
|
|
+ const { data: salaryData, isLoading: isLoadingSalary } = useQuery({
|
|
|
|
|
+ queryKey: ['statistics', 'salary-distribution'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await enterpriseStatisticsClient['salary-distribution'].$get()
|
|
|
|
|
+ return await response.json()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const isLoading = isLoadingDisability || isLoadingGender || isLoadingAge ||
|
|
|
|
|
+ isLoadingHousehold || isLoadingJobStatus || isLoadingSalary
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<YongrenTabBarLayout activeKey="statistics">
|
|
<YongrenTabBarLayout activeKey="statistics">
|
|
|
<ScrollView
|
|
<ScrollView
|
|
@@ -25,9 +92,237 @@ const Statistics: React.FC<StatisticsProps> = () => {
|
|
|
fixed={true}
|
|
fixed={true}
|
|
|
placeholder={true}
|
|
placeholder={true}
|
|
|
/>
|
|
/>
|
|
|
- <View className="p-4">
|
|
|
|
|
- <Text className="text-xl font-bold">数据统计</Text>
|
|
|
|
|
- <Text className="text-gray-600 mt-2">企业数据统计页面(待实现)</Text>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {/* 时间筛选 */}
|
|
|
|
|
+ <View className="flex justify-between items-center mb-4 pt-4">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700">数据统计</Text>
|
|
|
|
|
+ <View className="flex items-center bg-gray-100 rounded-lg px-3 py-1">
|
|
|
|
|
+ <Text className="text-sm text-gray-700 mr-2">2023年11月</Text>
|
|
|
|
|
+ <Text className="fas fa-chevron-down text-gray-500"></Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 统计卡片 */}
|
|
|
|
|
+ <View className="grid grid-cols-2 gap-3 mb-4">
|
|
|
|
|
+ <View className="stat-card bg-white rounded-lg p-3 shadow-sm">
|
|
|
|
|
+ <Text className="text-sm text-gray-600 mb-2">在职人数</Text>
|
|
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">24</Text>
|
|
|
|
|
+ <Text className="text-xs text-green-500 mt-1">↑ 比上月增加2人</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="stat-card bg-white rounded-lg p-3 shadow-sm">
|
|
|
|
|
+ <Text className="text-sm text-gray-600 mb-2">平均薪资</Text>
|
|
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">¥4,650</Text>
|
|
|
|
|
+ <Text className="text-xs text-green-500 mt-1">↑ 比上月增加¥150</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="stat-card bg-white rounded-lg p-3 shadow-sm">
|
|
|
|
|
+ <Text className="text-sm text-gray-600 mb-2">在职率</Text>
|
|
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">92%</Text>
|
|
|
|
|
+ <Text className="text-xs text-green-500 mt-1">↑ 比上月提升3%</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="stat-card bg-white rounded-lg p-3 shadow-sm">
|
|
|
|
|
+ <Text className="text-sm text-gray-600 mb-2">新增人数</Text>
|
|
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">3</Text>
|
|
|
|
|
+ <Text className="text-xs text-red-500 mt-1">↓ 比上月减少1人</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 残疾类型分布 */}
|
|
|
|
|
+ <View className="card bg-white p-4 mb-4 rounded-lg shadow-sm">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">残疾类型分布</Text>
|
|
|
|
|
+ {isLoadingDisability ? (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">加载中...</Text>
|
|
|
|
|
+ ) : disabilityData?.stats && disabilityData.stats.length > 0 ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <View className="chart-container mb-2 h-40 relative">
|
|
|
|
|
+ {disabilityData.stats.map((item: DisabilityTypeDistributionResponse['stats'][0], index: number) => {
|
|
|
|
|
+ const maxValue = Math.max(...disabilityData.stats.map((s: DisabilityTypeDistributionResponse['stats'][0]) => s.value || 0))
|
|
|
|
|
+ const height = maxValue > 0 ? ((item.value || 0) / maxValue) * 160 : 0
|
|
|
|
|
+ const left = 20 + index * 50
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View
|
|
|
|
|
+ key={item.key}
|
|
|
|
|
+ className="chart-bar absolute bottom-0 bg-blue-500 rounded-t-lg"
|
|
|
|
|
+ style={{ left: `${left}px`, height: `${height}px`, width: '30px' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="flex justify-between text-xs text-gray-500">
|
|
|
|
|
+ {disabilityData.stats.map((item: DisabilityTypeDistributionResponse['stats'][0]) => (
|
|
|
|
|
+ <View key={item.key} className="flex flex-col items-center">
|
|
|
|
|
+ <Text>{item.key}</Text>
|
|
|
|
|
+ <Text className="text-xs text-gray-400">{item.percentage}%</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">暂无数据</Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 性别分布 */}
|
|
|
|
|
+ <View className="card bg-white p-4 mb-4 rounded-lg shadow-sm">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">性别分布</Text>
|
|
|
|
|
+ {isLoadingGender ? (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">加载中...</Text>
|
|
|
|
|
+ ) : genderData?.stats && genderData.stats.length > 0 ? (
|
|
|
|
|
+ <View className="bar-chart flex items-end justify-center gap-10 h-32">
|
|
|
|
|
+ {genderData.stats.map((item: GenderDistributionResponse['stats'][0]) => {
|
|
|
|
|
+ const maxValue = Math.max(...genderData.stats.map((s: GenderDistributionResponse['stats'][0]) => s.value || 0))
|
|
|
|
|
+ const height = maxValue > 0 ? ((item.value || 0) / maxValue) * 100 : 0
|
|
|
|
|
+ const color = item.key === '男' ? '#3b82f6' : '#ec4899'
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View key={item.key} className="bar-container flex flex-col items-center">
|
|
|
|
|
+ <Text className="bar-value text-sm font-semibold mb-1">{item.value}人</Text>
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="bar rounded-t-lg"
|
|
|
|
|
+ style={{ height: `${height}px`, width: '40px', backgroundColor: color }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Text className="bar-label text-xs text-gray-500 mt-2">
|
|
|
|
|
+ {item.key} ({item.percentage}%)
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">暂无数据</Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 年龄分布 */}
|
|
|
|
|
+ <View className="card bg-white p-4 mb-4 rounded-lg shadow-sm">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">年龄分布</Text>
|
|
|
|
|
+ {isLoadingAge ? (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">加载中...</Text>
|
|
|
|
|
+ ) : ageData?.stats && ageData.stats.length > 0 ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <View className="pie-chart-container flex justify-center mb-4">
|
|
|
|
|
+ {/* 简单的饼图表示 - 实际项目可替换为SVG饼图 */}
|
|
|
|
|
+ <View className="w-32 h-32 rounded-full border-4 border-gray-300 flex items-center justify-center">
|
|
|
|
|
+ <Text className="text-gray-500">饼图</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="pie-legend flex flex-wrap justify-center gap-3">
|
|
|
|
|
+ {ageData.stats.map((item: AgeDistributionResponse['stats'][0], index: number) => {
|
|
|
|
|
+ const colors = ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#8b5cf6']
|
|
|
|
|
+ const color = colors[index % colors.length]
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View key={item.key} className="legend-item flex items-center">
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="legend-color w-3 h-3 rounded-sm mr-2"
|
|
|
|
|
+ style={{ backgroundColor: color }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Text className="text-xs text-gray-700">
|
|
|
|
|
+ {item.key} ({item.percentage}%)
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">暂无数据</Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 户籍省份分布 */}
|
|
|
|
|
+ <View className="card bg-white p-4 mb-4 rounded-lg shadow-sm">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">户籍省份分布</Text>
|
|
|
|
|
+ {isLoadingHousehold ? (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">加载中...</Text>
|
|
|
|
|
+ ) : householdData?.stats && householdData.stats.length > 0 ? (
|
|
|
|
|
+ <View className="space-y-3">
|
|
|
|
|
+ {householdData.stats.slice(0, 6).map((item: HouseholdDistributionResponse['stats'][0], index: number) => {
|
|
|
|
|
+ const maxValue = Math.max(...householdData.stats.map((s: HouseholdDistributionResponse['stats'][0]) => s.value || 0))
|
|
|
|
|
+ const width = maxValue > 0 ? ((item.value || 0) / maxValue) * 100 : 0
|
|
|
|
|
+ const colors = ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#ec4899']
|
|
|
|
|
+ const color = colors[index % colors.length]
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View key={item.key}>
|
|
|
|
|
+ <View className="flex justify-between text-sm mb-1">
|
|
|
|
|
+ <Text className="text-gray-700">{item.key}</Text>
|
|
|
|
|
+ <Text className="text-gray-500">{item.value}人</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="progress-bar h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="progress-fill h-full rounded-full"
|
|
|
|
|
+ style={{ width: `${width}%`, backgroundColor: color }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">暂无数据</Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 在职状态统计 */}
|
|
|
|
|
+ <View className="card bg-white p-4 mb-4 rounded-lg shadow-sm">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">在职状态统计</Text>
|
|
|
|
|
+ {isLoadingJobStatus ? (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">加载中...</Text>
|
|
|
|
|
+ ) : jobStatusData?.stats && jobStatusData.stats.length > 0 ? (
|
|
|
|
|
+ <View className="flex items-center justify-center">
|
|
|
|
|
+ {/* 环形图占位 */}
|
|
|
|
|
+ <View className="w-32 h-32 rounded-full border-8 border-blue-500 border-t-transparent transform -rotate-45" />
|
|
|
|
|
+ <View className="ml-6">
|
|
|
|
|
+ {jobStatusData.stats.map((item: JobStatusDistributionResponse['stats'][0], index: number) => {
|
|
|
|
|
+ const colors = ['#3b82f6', '#f59e0b', '#ef4444', '#10b981']
|
|
|
|
|
+ const color = colors[index % colors.length]
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View key={item.key} className="flex items-center mb-2">
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="w-3 h-3 rounded-full mr-2"
|
|
|
|
|
+ style={{ backgroundColor: color }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Text className="text-sm text-gray-700">
|
|
|
|
|
+ {item.key}: {item.value}人 ({item.percentage}%)
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">暂无数据</Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 薪资分布 */}
|
|
|
|
|
+ <View className="card bg-white p-4 rounded-lg shadow-sm">
|
|
|
|
|
+ <Text className="font-semibold text-gray-700 mb-3">薪资分布</Text>
|
|
|
|
|
+ {isLoadingSalary ? (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">加载中...</Text>
|
|
|
|
|
+ ) : salaryData?.stats && salaryData.stats.length > 0 ? (
|
|
|
|
|
+ <View className="space-y-3">
|
|
|
|
|
+ {salaryData.stats.map((item: SalaryDistributionResponse['stats'][0], index: number) => {
|
|
|
|
|
+ const maxValue = Math.max(...salaryData.stats.map((s: SalaryDistributionResponse['stats'][0]) => s.value || 0))
|
|
|
|
|
+ const width = maxValue > 0 ? ((item.value || 0) / maxValue) * 100 : 0
|
|
|
|
|
+ const colors = ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444']
|
|
|
|
|
+ const color = colors[index % colors.length]
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View key={item.key}>
|
|
|
|
|
+ <View className="flex justify-between text-sm mb-1">
|
|
|
|
|
+ <Text className="text-gray-700">{item.key}</Text>
|
|
|
|
|
+ <Text className="text-gray-500">{item.value}人</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View className="progress-bar h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
|
|
|
+ <View
|
|
|
|
|
+ className="progress-fill h-full rounded-full"
|
|
|
|
|
+ style={{ width: `${width}%`, backgroundColor: color }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text className="text-gray-500 text-center py-4">暂无数据</Text>
|
|
|
|
|
+ )}
|
|
|
</View>
|
|
</View>
|
|
|
</ScrollView>
|
|
</ScrollView>
|
|
|
</YongrenTabBarLayout>
|
|
</YongrenTabBarLayout>
|