Explorar o código

feat: 复制共享资源到 mini/src/ (UI包整合阶段1)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname hai 2 días
pai
achega
b2c44f7682

+ 1 - 0
mini/src/api/authIndex.ts

@@ -0,0 +1 @@
+export { enterpriseAuthClient } from './enterpriseAuthClient';

+ 7 - 0
mini/src/api/companyClient.ts

@@ -0,0 +1,7 @@
+import type { companyEnterpriseRoutes } from '@d8d/allin-company-module';
+import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
+
+// 企业专用公司API客户端
+// 路径前缀 /api/v1/yongren/company
+// 类型定义从 @d8d/allin-company-module 导入 companyEnterpriseRoutes
+export const enterpriseCompanyClient = rpcClient<typeof companyEnterpriseRoutes>('/api/v1/yongren/company');

+ 4 - 0
mini/src/api/enterpriseAuthClient.ts

@@ -0,0 +1,4 @@
+import type { enterpriseAuthRoutes } from '@d8d/auth-module';
+import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
+
+export const enterpriseAuthClient = rpcClient<typeof enterpriseAuthRoutes>('/api/v1/yongren/auth'); 

+ 4 - 0
mini/src/api/enterpriseCompanyClient.ts

@@ -0,0 +1,4 @@
+import { companyEnterpriseRoutes } from '@d8d/allin-company-module';
+import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
+
+export const enterpriseCompanyClient = rpcClient<typeof companyEnterpriseRoutes>('/api/v1/yongren/company'); 

+ 4 - 0
mini/src/api/enterpriseDisabilityClient.ts

@@ -0,0 +1,4 @@
+import { personExtensionRoutes } from '@d8d/allin-disability-module/routes';
+import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
+
+export const enterpriseDisabilityClient = rpcClient<typeof personExtensionRoutes>('/api/v1/yongren/disability-person'); 

+ 6 - 0
mini/src/api/enterpriseOrderClient.ts

@@ -0,0 +1,6 @@
+import type { enterpriseOrderRoutes } from '@d8d/allin-order-module';
+import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
+
+// 注意:企业专用视频API通过enterpriseAuthMiddleware中间件保护,确保仅限企业用户访问
+// 路径前缀 /api/v1/yongren/order 在路由层配置
+export const enterpriseOrderClient = rpcClient<typeof enterpriseOrderRoutes>('/api/v1/yongren/order');

+ 8 - 0
mini/src/api/enterpriseStatisticsClient.ts

@@ -0,0 +1,8 @@
+// @ts-ignore
+import type { statisticsRoutes } from '@d8d/allin-statistics-module';
+import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
+
+// 注意:企业专用数据统计API通过enterpriseAuthMiddleware中间件保护,确保仅限企业用户访问
+// 重要安全要求:企业专用API强制从JWT token中的`companyId`字段获取企业ID,不接受查询参数,确保数据隔离安全
+// 路径前缀 /api/v1/yongren/statistics 在路由层配置
+export const enterpriseStatisticsClient = rpcClient<typeof statisticsRoutes>('/api/v1/yongren/statistics');

+ 13 - 0
mini/src/api/index.ts

@@ -0,0 +1,13 @@
+/**
+ * 统一 API 客户端导出文件
+ *
+ * 从 UI 包整合回主目录后,所有 API 客户端统一从此处导出
+ */
+
+export { enterpriseCompanyClient } from './enterpriseCompanyClient'
+export { enterpriseOrderClient } from './enterpriseOrderClient'
+export { orderClient } from './orderClient'
+// companyClient.ts 只导出 enterpriseCompanyClient,已在上面导出
+export { enterpriseStatisticsClient } from './enterpriseStatisticsClient'
+export { enterpriseDisabilityClient } from './enterpriseDisabilityClient'
+export { enterpriseAuthClient } from './enterpriseAuthClient'

+ 121 - 0
mini/src/api/orderClient.ts

@@ -0,0 +1,121 @@
+// 订单API客户端
+export const orderClient = {
+  // 模拟客户端方法,实际使用时替换为真实的RPC客户端
+  create: { $post: async () => ({}) },
+  update: { ':id': { $put: async () => ({}) } },
+  delete: { ':id': { $delete: async () => ({}) } },
+  list: { $get: async () => ({}) },
+  detail: { ':id': { $get: async () => ({}) } },
+  activate: { ':orderId': { $post: async () => ({}) } },
+  close: { ':orderId': { $post: async () => ({}) } },
+  ':orderId': { persons: { batch: { $post: async () => ({}) } } },
+  assets: {
+    create: { $post: async () => ({}) },
+    query: { $get: async () => ({}) },
+    delete: { ':id': { $delete: async () => ({}) } },
+  },
+}
+
+// 扩展的统计API方法
+export class OrderStatisticsClient {
+  private baseUrl: string
+
+  constructor(baseUrl: string = '/api/v1/yongren') {
+    this.baseUrl = baseUrl
+  }
+
+  // 获取打卡统计数据
+  async getCheckinStatistics(companyId: number) {
+    const response = await fetch(`${this.baseUrl}/order/checkin-statistics?companyId=${companyId}`)
+    if (!response.ok) {
+      throw new Error(`获取打卡统计数据失败: ${response.statusText}`)
+    }
+    return response.json()
+  }
+
+  // 获取视频统计数据
+  async getVideoStatistics(companyId: number, assetType?: string) {
+    let url = `${this.baseUrl}/order/video-statistics?companyId=${companyId}`
+    if (assetType) {
+      url += `&assetType=${assetType}`
+    }
+    const response = await fetch(url)
+    if (!response.ok) {
+      throw new Error(`获取视频统计数据失败: ${response.statusText}`)
+    }
+    return response.json()
+  }
+
+  // 获取企业订单列表
+  async getCompanyOrders(params: {
+    companyId: number
+    orderName?: string
+    orderStatus?: string
+    startDate?: string
+    endDate?: string
+    page?: number
+    limit?: number
+    sortBy?: string
+    sortOrder?: string
+  }) {
+    const queryParams = new URLSearchParams()
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== undefined) {
+        queryParams.append(key, String(value))
+      }
+    })
+
+    const response = await fetch(`${this.baseUrl}/order/company-orders?${queryParams}`)
+    if (!response.ok) {
+      throw new Error(`获取企业订单列表失败: ${response.statusText}`)
+    }
+    return response.json()
+  }
+
+  // 获取企业视频列表
+  async getCompanyVideos(params: {
+    companyId: number
+    assetType?: string
+    page?: number
+    pageSize?: number
+    sortBy?: string
+    sortOrder?: string
+  }) {
+    const queryParams = new URLSearchParams()
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== undefined) {
+        queryParams.append(key, String(value))
+      }
+    })
+
+    const response = await fetch(`${this.baseUrl}/order/company-videos?${queryParams}`)
+    if (!response.ok) {
+      throw new Error(`获取企业视频列表失败: ${response.statusText}`)
+    }
+    return response.json()
+  }
+
+  // 批量下载视频
+  async batchDownloadVideos(request: {
+    downloadScope: 'company' | 'person'
+    companyId?: number
+    personId?: number
+    assetTypes?: string[]
+    fileIds?: number[]
+  }) {
+    const response = await fetch(`${this.baseUrl}/order/batch-download`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(request),
+    })
+    if (!response.ok) {
+      throw new Error(`批量下载视频失败: ${response.statusText}`)
+    }
+    return response.json()
+  }
+}
+
+// 创建默认实例
+export const orderStatisticsClient = new OrderStatisticsClient()

+ 87 - 0
mini/src/components/YongrenTabBarLayout.tsx

@@ -0,0 +1,87 @@
+import React, { ReactNode } from 'react'
+import { View } from '@tarojs/components'
+import { TabBar, type TabBarItem } from '@d8d/mini-shared-ui-components/components/tab-bar'
+import Taro from '@tarojs/taro'
+
+export interface YongrenTabBarLayoutProps {
+  children: ReactNode
+  activeKey: string
+}
+
+const yongrenTabBarItems: TabBarItem[] = [
+  {
+    key: 'dashboard',
+    title: '首页',
+    iconClass: 'i-heroicons-home-20-solid',
+    selectedIconClass: 'i-heroicons-home-20-solid',
+  },
+  {
+    key: 'talent',
+    title: '人才',
+    iconClass: 'i-heroicons-user-group-20-solid',
+    selectedIconClass: 'i-heroicons-user-group-20-solid',
+  },
+  {
+    key: 'order',
+    title: '订单',
+    iconClass: 'i-heroicons-document-text-20-solid',
+    selectedIconClass: 'i-heroicons-document-text-20-solid',
+  },
+  {
+    key: 'statistics',
+    title: '数据',
+    iconClass: 'i-heroicons-chart-bar-20-solid',
+    selectedIconClass: 'i-heroicons-chart-bar-20-solid',
+  },
+  {
+    key: 'settings',
+    title: '设置',
+    iconClass: 'i-heroicons-cog-6-tooth-20-solid',
+    selectedIconClass: 'i-heroicons-cog-6-tooth-20-solid',
+  },
+]
+
+export const YongrenTabBarLayout: React.FC<YongrenTabBarLayoutProps> = ({ children, activeKey }) => {
+  const handleTabChange = (key: string) => {
+    // 使用 Taro 的导航 API 进行页面跳转
+    switch (key) {
+      case 'dashboard':
+        Taro.switchTab({ url: '/pages/yongren/dashboard/index' })
+        break
+      case 'talent':
+        Taro.switchTab({ url: '/pages/yongren/talent/list/index' })
+        break
+      case 'order':
+        Taro.switchTab({ url: '/pages/yongren/order/list/index' })
+        break
+      case 'statistics':
+        Taro.switchTab({ url: '/pages/yongren/statistics/index' })
+        break
+      case 'settings':
+        Taro.switchTab({ url: '/pages/yongren/settings/index' })
+        break
+      default:
+        break
+    }
+  }
+
+  return (
+    <View className="min-h-screen bg-gray-50 flex flex-col">
+      <View className="flex-1 flex flex-col">
+        {children}
+      </View>
+      <TabBar
+        items={yongrenTabBarItems}
+        activeKey={activeKey}
+        onChange={handleTabChange}
+        fixed={true}
+        safeArea={true}
+        color="#999"
+        selectedColor="#3b82f6"
+        backgroundColor="white"
+      />
+    </View>
+  )
+}
+
+export default YongrenTabBarLayout

+ 2 - 0
mini/src/hooks/index.ts

@@ -0,0 +1,2 @@
+export { AuthProvider, useAuth, queryClient, type User, type AuthContextType } from './useAuth'
+export { useRequireAuth } from './useRequireAuth'

+ 207 - 0
mini/src/hooks/useAuth.tsx

@@ -0,0 +1,207 @@
+import { createContext, useContext, PropsWithChildren } from 'react'
+import Taro from '@tarojs/taro'
+import { QueryClient, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { enterpriseAuthClient } from '../api'
+
+// 登录请求类型
+interface LoginRequest {
+  phone: string
+  password: string
+}
+
+// 用户类型定义 - 企业用户
+export interface User {
+  id: number
+  username: string
+  phone: string | null
+  email: string | null
+  nickname: string | null
+  name: string | null
+  avatarFileId: number | null
+  avatarFile?: {
+    id: number
+    name: string
+    fullUrl: string
+    type: string | null
+    size: number | null
+  } | null
+  companyId: number | null
+  company?: {
+    id: number
+    companyName: string
+    contactPerson: string | null
+    contactPhone: string | null
+  } | null
+  createdAt: string
+  updatedAt: string
+}
+
+// 企业用户注册可能由管理员创建,前端不提供注册接口
+interface RegisterRequest {
+  username: string
+  password: string
+}
+
+export interface AuthContextType {
+  user: User | null
+  login: (data: LoginRequest) => Promise<User>
+  logout: () => Promise<void>
+  register: (data: RegisterRequest) => Promise<User>
+  updateUser: (userData: Partial<User>) => void
+  isLoading: boolean
+  isLoggedIn: boolean
+}
+
+const AuthContext = createContext<AuthContextType | undefined>(undefined)
+
+// 导出queryClient以供外部使用(如果需要)
+export const queryClient = new QueryClient()
+
+export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
+  const queryClient = useQueryClient()
+
+  const { data: user, isLoading } = useQuery<User | null, Error>({
+    queryKey: ['currentUser'],
+    queryFn: async () => {
+      const token = Taro.getStorageSync('enterprise_token')
+      if (!token) {
+        return null
+      }
+      try {
+        const response = await enterpriseAuthClient.me.$get({})
+        if (response.status !== 200) {
+          throw new Error('获取用户信息失败')
+        }
+        const user = await response.json()
+        Taro.setStorageSync('enterpriseUserInfo', JSON.stringify(user))
+        return user
+      } catch (_error) {
+        Taro.removeStorageSync('enterprise_token')
+        Taro.removeStorageSync('enterpriseUserInfo')
+        return null
+      }
+    },
+    staleTime: Infinity, // 用户信息不常变动,设为无限期
+    refetchOnWindowFocus: false, // 失去焦点不重新获取
+    refetchOnReconnect: false, // 网络重连不重新获取
+  })
+
+  const loginMutation = useMutation<User, Error, LoginRequest>({
+    mutationFn: async (data) => {
+      const response = await enterpriseAuthClient.login.$post({ json: data })
+      if (response.status !== 200) {
+        throw new Error('登录失败')
+      }
+      const { token, user } = await response.json()
+      Taro.setStorageSync('enterprise_token', token)
+      Taro.setStorageSync('enterpriseUserInfo', JSON.stringify(user))
+      // if (refresh_token) {
+      //   Taro.setStorageSync('enterprise_refresh_token', refresh_token)
+      // }
+      return user
+    },
+    onSuccess: (newUser) => {
+      queryClient.setQueryData(['currentUser'], newUser)
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '登录失败,请检查用户名和密码',
+        icon: 'none',
+        duration: 2000,
+      })
+    },
+  })
+
+  const registerMutation = useMutation<User, Error, RegisterRequest>({
+    mutationFn: async () => {
+      // 企业用户注册由管理员创建,前端不提供注册接口
+      throw new Error('企业用户注册请联系管理员创建账户')
+    },
+    onSuccess: (newUser) => {
+      queryClient.setQueryData(['currentUser'], newUser)
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '企业用户注册请联系管理员',
+        icon: 'none',
+        duration: 3000,
+      })
+    },
+  })
+
+  const logoutMutation = useMutation<void, Error>({
+    mutationFn: async () => {
+      try {
+        const response = await enterpriseAuthClient.logout.$post({})
+        if (response.status !== 200) {
+          throw new Error('登出失败')
+        }
+      } catch (error) {
+        console.error('Logout error:', error)
+      } finally {
+        Taro.removeStorageSync('enterprise_token')
+        Taro.removeStorageSync('enterprise_refresh_token')
+        Taro.removeStorageSync('enterpriseUserInfo')
+      }
+    },
+    onSuccess: async () => {
+      // 先清除用户状态
+      queryClient.setQueryData(['currentUser'], null)
+      // 使用 reLaunch 关闭所有页面并跳转到登录页
+      // 添加延迟确保状态更新完成
+      await new Promise(resolve => setTimeout(resolve, 100))
+      Taro.reLaunch({ url: '/pages/login/index' })
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '登出失败',
+        icon: 'none',
+        duration: 2000,
+      })
+    },
+  })
+
+  const updateUserMutation = useMutation<User, Error, Partial<User>>({
+    mutationFn: async () => {
+      // 企业用户信息更新可能由管理员管理,前端不提供更新接口
+      throw new Error('企业用户信息更新请联系管理员')
+    },
+    onSuccess: (updatedUser) => {
+      queryClient.setQueryData(['currentUser'], updatedUser)
+      Taro.showToast({
+        title: '更新成功',
+        icon: 'success',
+        duration: 2000,
+      })
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '更新失败,请重试',
+        icon: 'none',
+        duration: 2000,
+      })
+    },
+  })
+
+  const updateUser = updateUserMutation.mutateAsync
+
+  const value = {
+    user: user || null,
+    login: loginMutation.mutateAsync,
+    logout: logoutMutation.mutateAsync,
+    register: registerMutation.mutateAsync,
+    updateUser,
+    isLoading: isLoading || loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending,
+    isLoggedIn: !!user,
+  }
+
+  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
+}
+
+export const useAuth = () => {
+  const context = useContext(AuthContext)
+  if (context === undefined) {
+    throw new Error('useAuth must be used within an AuthProvider')
+  }
+  return context
+}

+ 23 - 0
mini/src/hooks/useRequireAuth.ts

@@ -0,0 +1,23 @@
+import { useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { useAuth } from './useAuth'
+
+/**
+ * 要求认证的hook
+ * 如果用户未登录,则重定向到登录页
+ */
+export const useRequireAuth = () => {
+  const { isLoggedIn, isLoading } = useAuth()
+
+  useEffect(() => {
+    // 只有在非loading状态且未登录时才跳转
+    if (!isLoading && !isLoggedIn) {
+      // 使用 reLaunch 而不是 redirectTo,确保在 TabBar 页面也能正常跳转
+      Taro.reLaunch({
+        url: '/pages/login/index'
+      })
+    }
+  }, [isLoggedIn, isLoading])
+
+  return { isLoggedIn, isLoading }
+}

+ 32 - 0
mini/src/types/orderTypes.ts

@@ -0,0 +1,32 @@
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { enterpriseOrderClient } from './enterpriseOrderClient';
+
+// 订单列表查询参数类型(企业专用)
+export type CompanyOrdersQueryParams = InferRequestType<typeof enterpriseOrderClient['company-orders']['$get']>['query'];
+
+// 分页响应类型
+export type PaginatedResponse<T> = {
+  data: T[];
+  total: number;
+};
+
+// 使用Hono类型推导
+export type OrderDetailResponse = InferResponseType<typeof enterpriseOrderClient.detail[':id']['$get'], 200>;
+export type OrderListResponse = InferResponseType<typeof enterpriseOrderClient['company-orders']['$get'], 200>;
+export type OrderData = InferResponseType<typeof enterpriseOrderClient['company-orders']['$get'], 200>['data'][0];
+
+
+// 企业专用扩展API
+export type CheckinStatisticsResponse = InferResponseType<typeof enterpriseOrderClient['checkin-statistics']['$get'], 200>;
+export type VideoStatisticsResponse = InferResponseType<typeof enterpriseOrderClient['video-statistics']['$get'], 200>;
+// 订单统计API路径: /company-orders/:id/stats -> client key: 'company-orders' -> ':id' -> 'stats'
+export type OrderStatsResponse = InferResponseType<typeof enterpriseOrderClient['company-orders'][':id']['stats']['$get'], 200>;
+export type CompanyVideosResponse = InferResponseType<typeof enterpriseOrderClient['company-videos']['$get'], 200>;
+export type BatchDownloadRequest = InferRequestType<typeof enterpriseOrderClient['batch-download']['$post']>['json'];
+export type UpdateVideoStatusRequest = InferRequestType<typeof enterpriseOrderClient.videos[':id']['status']['$put']>['json'];
+
+// 查询参数类型
+export type CheckinStatisticsParams = InferRequestType<typeof enterpriseOrderClient['checkin-statistics']['$get']>['query'];
+export type VideoStatisticsParams = InferRequestType<typeof enterpriseOrderClient['video-statistics']['$get']>['query'];
+export type CompanyVideosParams = InferRequestType<typeof enterpriseOrderClient['company-videos']['$get']>['query'];
+export type BatchDownloadParams = InferRequestType<typeof enterpriseOrderClient['batch-download']['$post']>['json'];

+ 39 - 0
mini/src/types/settingsTypes.ts

@@ -0,0 +1,39 @@
+// ==================== 视频资产类型定义 ====================
+
+// 视频类型枚举
+export type VideoAssetType = 'salary_video' | 'tax_video' | 'checkin_video' | 'work_video';
+
+// 视频状态枚举
+export type VideoStatus = 'pending' | 'verified' | 'rejected';
+
+// 视频卡片展示数据
+export interface VideoCardData {
+  id: string;
+  title: string;
+  description: string;
+  assetType: VideoAssetType;
+  status: VideoStatus;
+  fileSize: number;
+  duration: number;
+  uploadedAt: string;
+  thumbnailUrl?: string;
+  fileUrl: string;
+  talentName?: string;
+  orderId?: string;
+}
+
+// 视频统计卡片数据
+export interface VideoStatisticsData {
+  totalCount: number;
+  salaryVideoCount: number;
+  taxVideoCount: number;
+  checkinVideoCount: number;
+  workVideoCount: number;
+}
+
+// 视频分类标签
+export interface VideoCategoryTab {
+  key: 'all' | VideoAssetType;
+  label: string;
+  count: number;
+}

+ 35 - 0
mini/src/types/statisticsTypes.ts

@@ -0,0 +1,35 @@
+import type { InferResponseType } from 'hono/client';
+import { enterpriseStatisticsClient } from './enterpriseStatisticsClient';
+
+// 使用Hono类型推导 - 注意正确的属性访问语法
+export type DisabilityTypeDistributionResponse = InferResponseType<typeof enterpriseStatisticsClient['disability-type-distribution']['$get'], 200>;
+export type GenderDistributionResponse = InferResponseType<typeof enterpriseStatisticsClient['gender-distribution']['$get'], 200>;
+export type AgeDistributionResponse = InferResponseType<typeof enterpriseStatisticsClient['age-distribution']['$get'], 200>;
+export type HouseholdDistributionResponse = InferResponseType<typeof enterpriseStatisticsClient['household-distribution']['$get'], 200>;
+export type JobStatusDistributionResponse = InferResponseType<typeof enterpriseStatisticsClient['job-status-distribution']['$get'], 200>;
+export type SalaryDistributionResponse = InferResponseType<typeof enterpriseStatisticsClient['salary-distribution']['$get'], 200>;
+
+// 统计卡片响应类型
+export type EmploymentCountResponse = InferResponseType<typeof enterpriseStatisticsClient['employment-count']['$get'], 200>;
+export type AverageSalaryResponse = InferResponseType<typeof enterpriseStatisticsClient['average-salary']['$get'], 200>;
+export type EmploymentRateResponse = InferResponseType<typeof enterpriseStatisticsClient['employment-rate']['$get'], 200>;
+
+// API 错误响应类型
+export interface ApiErrorResponse {
+  code: number;
+  message: string;
+}
+
+// 查询参数类型推导(支持年月查询参数)
+export type DisabilityTypeDistributionParams = Record<string, never>;
+export type GenderDistributionParams = Record<string, never>;
+export type AgeDistributionParams = Record<string, never>;
+export type HouseholdDistributionParams = Record<string, never>;
+export type JobStatusDistributionParams = Record<string, never>;
+export type SalaryDistributionParams = Record<string, never>;
+
+// 年月查询参数类型(新增的统计卡片 API)
+export interface YearMonthParams {
+  year?: number;
+  month?: number;
+}