Przeglądaj źródła

✨ feat(yongren-statistics-ui): 新增数据统计UI组件包

- 新增Statistics组件,使用YongrenTabBarLayout布局
- 新增API客户端占位文件,为后续功能做准备
- 新增组件导出入口文件
- 添加共享UI依赖包支持

✨ feat(yongren-talent-management-ui): 新增人才管理UI组件包

- 新增TalentManagement组件,包含完整的人才列表、搜索、筛选功能
- 新增CSS样式文件,包含头像颜色、卡片样式、加载动画等
- 新增企业残疾人API客户端,支持人才列表查询
- 新增组件导出入口文件
- 添加共享UI依赖包支持
- 实现人才列表分页、搜索防抖、下拉刷新功能
- 集成企业认证UI组件,实现登录状态控制
yourname 1 miesiąc temu
rodzic
commit
92e63a9e78

+ 2 - 0
mini-ui-packages/yongren-statistics-ui/package.json

@@ -27,6 +27,8 @@
   },
   "dependencies": {
     "@d8d/mini-shared-ui-components": "workspace:*",
+    "@d8d/yongren-shared-ui": "workspace:*",
+    "@d8d/mini-enterprise-auth-ui": "workspace:*",
     "@d8d/server": "workspace:*",
     "@tarojs/components": "4.1.4",
     "@tarojs/react": "4.1.4",

+ 20 - 0
mini-ui-packages/yongren-statistics-ui/src/Statistics.tsx

@@ -0,0 +1,20 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { YongrenTabBarLayout } from '@d8d/yongren-shared-ui'
+
+export interface StatisticsProps {
+  // 组件属性定义(目前为空)
+}
+
+const Statistics: React.FC<StatisticsProps> = () => {
+  return (
+    <YongrenTabBarLayout activeKey="statistics">
+      <View className="p-4">
+        <Text className="text-xl font-bold">数据统计</Text>
+        <Text className="text-gray-600 mt-2">企业数据统计页面(待实现)</Text>
+      </View>
+    </YongrenTabBarLayout>
+  )
+}
+
+export default Statistics

+ 10 - 0
mini-ui-packages/yongren-statistics-ui/src/api/index.ts

@@ -0,0 +1,10 @@
+// Statistics API 客户端
+// 注意:目前statistics功能尚未实现,此文件为占位符
+
+export const statisticsClient = {
+  // 统计相关API将在后续实现
+}
+
+export type StatisticsRoutes = {
+  // 路由类型定义将在后续实现
+}

+ 2 - 0
mini-ui-packages/yongren-statistics-ui/src/index.ts

@@ -0,0 +1,2 @@
+export { default as Statistics } from './Statistics'
+export type { StatisticsProps } from './Statistics'

+ 2 - 0
mini-ui-packages/yongren-talent-management-ui/package.json

@@ -27,6 +27,8 @@
   },
   "dependencies": {
     "@d8d/mini-shared-ui-components": "workspace:*",
+    "@d8d/yongren-shared-ui": "workspace:*",
+    "@d8d/mini-enterprise-auth-ui": "workspace:*",
     "@d8d/server": "workspace:*",
     "@tarojs/components": "4.1.4",
     "@tarojs/react": "4.1.4",

+ 58 - 0
mini-ui-packages/yongren-talent-management-ui/src/TalentManagement.css

@@ -0,0 +1,58 @@
+/* 人才列表页样式 */
+
+/* 头像颜色类 */
+.name-avatar.blue {
+  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
+}
+.name-avatar.green {
+  background: linear-gradient(135deg, #10b981, #059669);
+}
+.name-avatar.purple {
+  background: linear-gradient(135deg, #8b5cf6, #7c3aed);
+}
+.name-avatar.orange {
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+}
+.name-avatar.red {
+  background: linear-gradient(135deg, #ef4444, #dc2626);
+}
+.name-avatar.teal {
+  background: linear-gradient(135deg, #14b8a6, #0d9488);
+}
+
+/* 卡片样式 */
+.card {
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.card:active {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+}
+
+/* 筛选标签激活状态 */
+.filter-tag-active {
+  background-color: rgb(219 234 254);
+  color: rgb(30 64 175);
+}
+
+.filter-tag-inactive {
+  background-color: rgb(243 244 246);
+  color: rgb(55 65 81);
+}
+
+/* 加载动画 */
+@keyframes pulse-bg {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.5;
+  }
+}
+
+.animate-pulse {
+  animation: pulse-bg 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}

+ 355 - 0
mini-ui-packages/yongren-talent-management-ui/src/TalentManagement.tsx

@@ -0,0 +1,355 @@
+import React, { useEffect, useState } from 'react'
+import { View, Text, Input, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+import { YongrenTabBarLayout } from '@d8d/yongren-shared-ui'
+import { PageContainer } from '@d8d/mini-shared-ui-components'
+import { enterpriseDisabilityClient } from './api'
+import { useAuth, useRequireAuth } from '@d8d/mini-enterprise-auth-ui'
+import './TalentManagement.css'
+
+export interface TalentManagementProps {
+  // 组件属性定义(目前为空)
+}
+
+// 企业专用人才列表项类型 - 匹配CompanyPersonListItemSchema
+interface CompanyPersonListItem {
+  personId: number
+  name: string
+  gender: string
+  idCard: string
+  disabilityType: string
+  disabilityLevel: string
+  phone: string | null
+  jobStatus: string
+  latestJoinDate: string | null
+  orderName: string | null
+}
+
+// 企业专用人才列表API响应类型 - 匹配CompanyPersonListResponseSchema
+interface CompanyPersonListResponse {
+  data: CompanyPersonListItem[]
+  pagination: {
+    page: number
+    limit: number
+    total: number
+    totalPages: number
+  }
+}
+
+// useQuery返回的数据结构
+interface TalentListData {
+  data: CompanyPersonListItem[]
+  total: number
+  page: number
+  limit: number
+  totalPages: number
+}
+
+const TalentManagement: React.FC<TalentManagementProps> = () => {
+  const { user: _user } = useAuth()
+  const { isLoggedIn } = useRequireAuth()
+  const queryClient = useQueryClient()
+
+  // 搜索和筛选状态
+  const [searchText, setSearchText] = useState('')
+  const [activeStatus, setActiveStatus] = useState<string>('全部')
+  const [activeDisabilityType, setActiveDisabilityType] = useState<string>('')
+  const [page, setPage] = useState(1)
+  const limit = 20
+
+  // 搜索参数防抖
+  const [debouncedSearchText, setDebouncedSearchText] = useState('')
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      setDebouncedSearchText(searchText)
+      setPage(1) // 搜索时重置到第一页
+    }, 500)
+    return () => clearTimeout(timer)
+  }, [searchText])
+
+  // 构建查询参数(企业专用API使用camelCase参数名)
+  const queryParams = {
+    search: debouncedSearchText || undefined,
+    jobStatus: activeStatus !== '全部' ? activeStatus : undefined,
+    disabilityType: activeDisabilityType || undefined,
+    page,
+    limit
+  }
+
+  // 获取人才列表数据(使用企业专用API)
+  const { data: talentList, isLoading, error, refetch } = useQuery({
+    queryKey: ['talentList', queryParams],
+    queryFn: async () => {
+      const response = await enterpriseDisabilityClient.$get({
+        query: queryParams
+      })
+      if (response.status !== 200) {
+        throw new Error('获取人才列表失败')
+      }
+      const result = await response.json() as CompanyPersonListResponse
+      // API返回结构:{ data: [...], pagination: { total, page, limit, totalPages } }
+      const data = result?.data || []
+      const pagination = result?.pagination || { total: 0, page: 1, limit: 20, totalPages: 0 }
+      // 转换为扁平结构以便使用
+      return {
+        data,
+        total: pagination.total,
+        page: pagination.page,
+        limit: pagination.limit,
+        totalPages: pagination.totalPages
+      }
+    },
+    enabled: isLoggedIn, // 只有登录后才获取数据
+    refetchOnWindowFocus: false
+  })
+
+  // 下拉刷新
+  const [refreshing, setRefreshing] = useState(false)
+  const onRefresh = async () => {
+    setRefreshing(true)
+    try {
+      await queryClient.invalidateQueries({ queryKey: ['talentList'] })
+      await refetch()
+    } finally {
+      setTimeout(() => setRefreshing(false), 1000)
+    }
+  }
+
+  // 页面加载时设置标题
+  useEffect(() => {
+    Taro.setNavigationBarTitle({
+      title: '人才管理'
+    })
+  }, [])
+
+  // 状态标签列表
+  const statusTags = ['全部', '在职', '待入职', '离职']
+  const disabilityTypeTags = ['肢体残疾', '听力残疾', '视力残疾', '言语残疾', '智力残疾', '精神残疾']
+
+  // 处理状态筛选点击
+  const handleStatusClick = (status: string) => {
+    setActiveStatus(status)
+    setPage(1)
+  }
+
+  // 处理残疾类型筛选点击
+  const handleDisabilityTypeClick = (type: string) => {
+    setActiveDisabilityType(activeDisabilityType === type ? '' : type)
+    setPage(1)
+  }
+
+  // 处理搜索输入
+  const handleSearchChange = (e: any) => {
+    setSearchText(e.detail.value)
+  }
+
+  // 分页处理
+  const handlePrevPage = () => {
+    if (page > 1) {
+      setPage(page - 1)
+    }
+  }
+
+  const handleNextPage = () => {
+    if (talentList && page < Math.ceil(talentList.total / limit)) {
+      setPage(page + 1)
+    }
+  }
+
+  // 处理人才卡片点击跳转
+  const handleTalentClick = (talentId: number) => {
+    Taro.navigateTo({
+      url: `/pages/yongren/talent/detail/index?id=${talentId}`
+    })
+  }
+
+  // 获取头像颜色
+  const getAvatarColor = (id: number) => {
+    const colors = ['blue', 'green', 'purple', 'orange', 'red', 'teal']
+    const index = id % colors.length
+    return colors[index]
+  }
+
+  return (
+    <YongrenTabBarLayout activeKey="talent">
+      <PageContainer padding={false} className="pb-0">
+        <ScrollView
+          className="h-[calc(100vh-120px)] overflow-y-auto"
+          scrollY
+          refresherEnabled
+          refresherTriggered={refreshing}
+          onRefresherRefresh={onRefresh}
+        >
+          {/* 搜索和筛选区域 - 对照原型第434-447行 */}
+          <View className="p-4 border-b border-gray-200">
+            <View className="flex items-center bg-gray-100 rounded-lg px-4 py-2 mb-3">
+              <Text className="i-heroicons-magnifying-glass-20-solid text-gray-400 mr-2" />
+              <Input
+                type="text"
+                placeholder="搜索姓名、残疾证号..."
+                className="w-full bg-transparent outline-none text-sm"
+                value={searchText}
+                onInput={handleSearchChange}
+              />
+            </View>
+            <ScrollView className="flex space-x-2 pb-2" scrollX>
+              {statusTags.map((status) => (
+                <View
+                  key={status}
+                  className={`text-xs px-3 py-1 rounded-full whitespace-nowrap ${
+                    activeStatus === status
+                      ? 'bg-blue-100 text-blue-800'
+                      : 'bg-gray-100 text-gray-800'
+                  }`}
+                  onClick={() => handleStatusClick(status)}
+                >
+                  {status}
+                </View>
+              ))}
+            </ScrollView>
+            <ScrollView className="flex space-x-2 mt-2" scrollX>
+              {disabilityTypeTags.map((type) => (
+                <View
+                  key={type}
+                  className={`text-xs px-3 py-1 rounded-full whitespace-nowrap ${
+                    activeDisabilityType === type
+                      ? 'bg-blue-100 text-blue-800'
+                      : 'bg-gray-100 text-gray-800'
+                  }`}
+                  onClick={() => handleDisabilityTypeClick(type)}
+                >
+                  {type}
+                </View>
+              ))}
+            </ScrollView>
+          </View>
+
+          {/* 人才列表区域 - 对照原型第451-560行 */}
+          <View className="p-4">
+            <View className="flex justify-between items-center mb-4">
+              <Text className="font-semibold text-gray-700">
+                全部人才 ({talentList?.total || 0})
+              </Text>
+              <View className="flex space-x-2">
+                <View className="text-gray-500">
+                  <Text className="i-heroicons-funnel-20-solid" />
+                </View>
+                <View className="text-gray-500">
+                  <Text className="i-heroicons-arrows-up-down-20-solid" />
+                </View>
+              </View>
+            </View>
+
+            {isLoading ? (
+              // 加载状态
+              <View className="space-y-3">
+                {[1, 2, 3].map((i) => (
+                  <View key={i} className="bg-white p-4 rounded-lg animate-pulse flex items-center">
+                    <View className="w-10 h-10 bg-gray-200 rounded-full" />
+                    <View className="flex-1 ml-3">
+                      <View className="h-4 bg-gray-200 rounded w-1/3 mb-2" />
+                      <View className="h-3 bg-gray-200 rounded w-1/2" />
+                      <View className="flex justify-between mt-2">
+                        <View className="h-3 bg-gray-200 rounded w-1/4" />
+                        <View className="h-3 bg-gray-200 rounded w-1/4" />
+                      </View>
+                    </View>
+                  </View>
+                ))}
+              </View>
+            ) : error ? (
+              // 错误状态
+              <View className="bg-white p-4 rounded-lg text-center">
+                <Text className="text-red-500 text-sm">加载失败: {(error as Error).message}</Text>
+                <View
+                  className="mt-2 bg-blue-500 text-white text-xs px-3 py-1 rounded-full inline-block"
+                  onClick={() => refetch()}
+                >
+                  重试
+                </View>
+              </View>
+            ) : talentList && talentList.data.length > 0 ? (
+              // 人才列表
+              <View className="space-y-3">
+                {talentList.data.map((talent) => (
+                  <View
+                    key={talent.personId}
+                    className="card bg-white p-4 flex items-center cursor-pointer active:bg-gray-50"
+                    onClick={() => handleTalentClick(talent.personId)}
+                  >
+                    <View className={`name-avatar ${getAvatarColor(talent.personId)} w-10 h-10 rounded-full flex items-center justify-center`}>
+                      <Text className="text-white font-semibold">
+                        {talent.name.charAt(0)}
+                      </Text>
+                    </View>
+                    <View className="flex-1 ml-3">
+                      <View className="flex justify-between items-start">
+                        <View>
+                          <Text className="font-semibold text-gray-800">{talent.name}</Text>
+                          <Text className="text-xs text-gray-500">
+                            {talent.disabilityType || '未指定'} · {talent.disabilityLevel || '未分级'} · {talent.gender} · 未知岁
+                          </Text>
+                        </View>
+                        <Text className={`text-xs px-2 py-1 rounded-full ${
+                          talent.jobStatus === '在职'
+                            ? 'bg-green-100 text-green-800'
+                            : talent.jobStatus === '待入职'
+                            ? 'bg-yellow-100 text-yellow-800'
+                            : 'bg-gray-100 text-gray-800'
+                        }`}>
+                          {talent.jobStatus}
+                        </Text>
+                      </View>
+                      <View className="mt-2 flex justify-between text-xs text-gray-500">
+                        <Text>入职: {talent.latestJoinDate ? new Date(talent.latestJoinDate).toLocaleDateString() : '未入职'}</Text>
+                        <Text>薪资: 待定</Text>
+                      </View>
+                    </View>
+                  </View>
+                ))}
+
+                {/* 分页控件 */}
+                {talentList.total > limit && (
+                  <View className="flex justify-between items-center mt-4 pt-4 border-t border-gray-200">
+                    <View
+                      className={`text-xs px-3 py-1 rounded ${page === 1 ? 'text-gray-400' : 'text-blue-600'}`}
+                      onClick={handlePrevPage}
+                    >
+                      上一页
+                    </View>
+                    <Text className="text-xs text-gray-600">
+                      第 {page} 页 / 共 {Math.ceil(talentList.total / limit)} 页
+                    </Text>
+                    <View
+                      className={`text-xs px-3 py-1 rounded ${
+                        talentList && page >= Math.ceil(talentList.total / limit)
+                          ? 'text-gray-400'
+                          : 'text-blue-600'
+                      }`}
+                      onClick={handleNextPage}
+                    >
+                      下一页
+                    </View>
+                  </View>
+                )}
+              </View>
+            ) : (
+              // 空状态
+              <View className="bg-white p-4 rounded-lg text-center">
+                <Text className="text-gray-500 text-sm">暂无人才数据</Text>
+                {debouncedSearchText && (
+                  <Text className="text-gray-400 text-xs mt-1">
+                    尝试调整搜索条件
+                  </Text>
+                )}
+              </View>
+            )}
+          </View>
+        </ScrollView>
+      </PageContainer>
+    </YongrenTabBarLayout>
+  )
+}
+
+export default TalentManagement

+ 44 - 0
mini-ui-packages/yongren-talent-management-ui/src/api/enterpriseDisabilityClient.ts

@@ -0,0 +1,44 @@
+import type { EnterpriseDisabilityRoutes } from '@d8d/server';
+import { createRpcClient } from '@d8d/mini-shared-ui-components';
+
+export class EnterpriseDisabilityClientManager {
+  private static instance: EnterpriseDisabilityClientManager;
+  private client: ReturnType<typeof createRpcClient<typeof EnterpriseDisabilityRoutes>> | null = null;
+
+  private constructor() {}
+
+  public static getInstance(): EnterpriseDisabilityClientManager {
+    if (!EnterpriseDisabilityClientManager.instance) {
+      EnterpriseDisabilityClientManager.instance = new EnterpriseDisabilityClientManager();
+    }
+    return EnterpriseDisabilityClientManager.instance;
+  }
+
+  // 初始化客户端
+  public init(baseUrl: string = '/'): ReturnType<typeof createRpcClient<typeof EnterpriseDisabilityRoutes>> {
+    return this.client = createRpcClient<typeof EnterpriseDisabilityRoutes>({ apiBaseUrl: baseUrl });
+  }
+
+  // 获取客户端实例
+  public get(): ReturnType<typeof createRpcClient<typeof EnterpriseDisabilityRoutes>> {
+    if (!this.client) {
+      return this.init()
+    }
+    return this.client;
+  }
+
+  // 重置客户端(用于测试或重新初始化)
+  public reset(): void {
+    this.client = null;
+  }
+}
+
+// 导出单例实例
+const enterpriseDisabilityClientManager = EnterpriseDisabilityClientManager.getInstance();
+
+// 导出默认客户端实例(延迟初始化)
+export const enterpriseDisabilityClient = enterpriseDisabilityClientManager.get()
+
+export {
+  enterpriseDisabilityClientManager
+}

+ 2 - 0
mini-ui-packages/yongren-talent-management-ui/src/api/index.ts

@@ -0,0 +1,2 @@
+export { enterpriseDisabilityClient, enterpriseDisabilityClientManager } from './enterpriseDisabilityClient';
+export type { EnterpriseDisabilityRoutes } from '@d8d/server';

+ 2 - 0
mini-ui-packages/yongren-talent-management-ui/src/index.ts

@@ -0,0 +1,2 @@
+export { default as TalentManagement } from './TalentManagement'
+export type { TalentManagementProps } from './TalentManagement'