Forráskód Böngészése

✨ feat(mini-talent): 初始化用人方小程序项目结构

- 创建完整的Taro小程序项目基础配置
- 集成企业用户认证系统,包含登录、注册、用户信息管理功能
- 添加用人方专用页面模块:仪表板、人才管理、订单管理、数据统计、设置
- 配置多平台支持(微信小程序、H5),包含开发和生产环境配置
- 集成Tailwind CSS样式系统和图标库,提供统一的UI组件
- 添加完整的测试配置(Jest + Testing Library),支持组件和API测试
- 配置代码质量工具:ESLint、Stylelint、Commitlint、Husky
- 实现MinIO文件上传功能,支持H5和小程序双平台
- 集成React Query进行状态管理和API调用
- 添加响应式设计和滚动条样式规范
- 配置TypeScript支持,提供完整的类型定义
yourname 3 hete
szülő
commit
1055ee948c
80 módosított fájl, 4705 hozzáadás és 0 törlés
  1. 12 0
      mini-talent/.editorconfig
  2. 1 0
      mini-talent/.env.test
  3. 7 0
      mini-talent/.eslintrc
  4. 10 0
      mini-talent/.gitignore
  5. 4 0
      mini-talent/.husky/commit-msg
  6. 195 0
      mini-talent/README.md
  7. 17 0
      mini-talent/babel.config.js
  8. 1 0
      mini-talent/commitlint.config.mjs
  9. 31 0
      mini-talent/config/dev.ts
  10. 143 0
      mini-talent/config/index.ts
  11. 39 0
      mini-talent/config/prod.ts
  12. 38 0
      mini-talent/jest.config.js
  13. 135 0
      mini-talent/package.json
  14. 5 0
      mini-talent/postcss.config.js
  15. 15 0
      mini-talent/project.config.json
  16. 35 0
      mini-talent/src/api.ts
  17. 51 0
      mini-talent/src/app.config.ts
  18. 193 0
      mini-talent/src/app.css
  19. 23 0
      mini-talent/src/app.tsx
  20. 131 0
      mini-talent/src/components/ui/avatar-upload.tsx
  21. 46 0
      mini-talent/src/components/ui/button.tsx
  22. 54 0
      mini-talent/src/components/ui/card.tsx
  23. 95 0
      mini-talent/src/components/ui/dialog.tsx
  24. 168 0
      mini-talent/src/components/ui/form.tsx
  25. 127 0
      mini-talent/src/components/ui/image.tsx
  26. 102 0
      mini-talent/src/components/ui/input.tsx
  27. 55 0
      mini-talent/src/components/ui/label.tsx
  28. 230 0
      mini-talent/src/components/ui/navbar.tsx
  29. 37 0
      mini-talent/src/components/ui/page-container.tsx
  30. 155 0
      mini-talent/src/components/ui/tab-bar.tsx
  31. 58 0
      mini-talent/src/components/ui/user-status-bar.tsx
  32. 29 0
      mini-talent/src/hooks/useRequireAuth.ts
  33. 31 0
      mini-talent/src/index.html
  34. 2 0
      mini-talent/src/pages/login/index.config.ts
  35. 1 0
      mini-talent/src/pages/login/index.css
  36. 3 0
      mini-talent/src/pages/login/index.tsx
  37. 2 0
      mini-talent/src/pages/profile/index.config.ts
  38. 1 0
      mini-talent/src/pages/profile/index.css
  39. 3 0
      mini-talent/src/pages/profile/index.tsx
  40. 2 0
      mini-talent/src/pages/yongren/dashboard/index.config.ts
  41. 1 0
      mini-talent/src/pages/yongren/dashboard/index.css
  42. 3 0
      mini-talent/src/pages/yongren/dashboard/index.tsx
  43. 4 0
      mini-talent/src/pages/yongren/order/detail/index.config.ts
  44. 3 0
      mini-talent/src/pages/yongren/order/detail/index.tsx
  45. 4 0
      mini-talent/src/pages/yongren/order/list/index.config.ts
  46. 3 0
      mini-talent/src/pages/yongren/order/list/index.tsx
  47. 4 0
      mini-talent/src/pages/yongren/settings/index.config.ts
  48. 3 0
      mini-talent/src/pages/yongren/settings/index.tsx
  49. 4 0
      mini-talent/src/pages/yongren/statistics/index.config.ts
  50. 3 0
      mini-talent/src/pages/yongren/statistics/index.tsx
  51. 4 0
      mini-talent/src/pages/yongren/talent/detail/index.config.ts
  52. 1 0
      mini-talent/src/pages/yongren/talent/detail/index.css
  53. 5 0
      mini-talent/src/pages/yongren/talent/detail/index.tsx
  54. 4 0
      mini-talent/src/pages/yongren/talent/list/index.config.ts
  55. 1 0
      mini-talent/src/pages/yongren/talent/list/index.css
  56. 4 0
      mini-talent/src/pages/yongren/talent/list/index.tsx
  57. 4 0
      mini-talent/src/pages/yongren/video/index.config.ts
  58. 9 0
      mini-talent/src/pages/yongren/video/index.tsx
  59. 26 0
      mini-talent/src/schemas/register.schema.ts
  60. 172 0
      mini-talent/src/utils/auth.tsx
  61. 2 0
      mini-talent/src/utils/cn.ts
  62. 79 0
      mini-talent/src/utils/headers-polyfill.js
  63. 879 0
      mini-talent/src/utils/minio.ts
  64. 2 0
      mini-talent/src/utils/platform.ts
  65. 88 0
      mini-talent/src/utils/response-polyfill.ts
  66. 182 0
      mini-talent/src/utils/rpc-client.ts
  67. 4 0
      mini-talent/stylelint.config.mjs
  68. 28 0
      mini-talent/tailwind.config.js
  69. 1 0
      mini-talent/tests/__mocks__/fileMock.js
  70. 1 0
      mini-talent/tests/__mocks__/styleMock.js
  71. 100 0
      mini-talent/tests/__mocks__/taroMock.ts
  72. 13 0
      mini-talent/tests/__snapshots__/example.test.tsx.snap
  73. 37 0
      mini-talent/tests/components/Button.test.tsx
  74. 43 0
      mini-talent/tests/example.test.tsx
  75. 440 0
      mini-talent/tests/setup.ts
  76. 59 0
      mini-talent/tests/yongren-api.test.ts
  77. 46 0
      mini-talent/tests/yongren-components.test.tsx
  78. 89 0
      mini-talent/tests/yongren-routes.test.ts
  79. 34 0
      mini-talent/tsconfig.json
  80. 29 0
      mini-talent/types/global.d.ts

+ 12 - 0
mini-talent/.editorconfig

@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false

+ 1 - 0
mini-talent/.env.test

@@ -0,0 +1 @@
+# TARO_APP_ID="测试环境下的小程序 AppID"

+ 7 - 0
mini-talent/.eslintrc

@@ -0,0 +1,7 @@
+{
+  "extends": ["taro/react"],
+  "rules": {
+    "react/jsx-uses-react": "off",
+    "react/react-in-jsx-scope": "off"
+  }
+}

+ 10 - 0
mini-talent/.gitignore

@@ -0,0 +1,10 @@
+dist/
+deploy_versions/
+.temp/
+.rn_temp/
+node_modules/
+.DS_Store
+.swc
+*.local
+!.env.development
+!.env.production

+ 4 - 0
mini-talent/.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+
+# 运行 commitlint 检查 commit message
+npx --no -- commitlint --edit ${1}

+ 195 - 0
mini-talent/README.md

@@ -0,0 +1,195 @@
+# 小程序用户认证Starter
+
+这是一个基于Taro的小程序用户认证starter项目,集成了完整的用户登录、注册功能,并连接到后端API。
+
+## 功能特性
+
+- ✅ 用户注册和登录
+- ✅ JWT Token认证
+- ✅ 用户信息管理
+- ✅ 响应式设计
+- ✅ 错误处理
+- ✅ 本地存储
+
+## 技术栈
+
+- **框架**: Taro 4.1.4
+- **语言**: TypeScript
+- **样式**: CSS
+- **HTTP请求**: Taro.request
+- **状态管理**: 本地存储
+
+## 项目结构
+
+```
+mini/
+├── src/
+│   ├── pages/
+│   │   ├── index/          # 首页
+│   │   ├── login/          # 登录页
+│   │   └── register/       # 注册页
+│   ├── utils/
+│   │   └── auth.ts         # 认证工具
+│   ├── api.ts              # API客户端
+│   └── app.config.ts       # 小程序配置
+├── config/
+│   ├── dev.ts              # 开发配置
+│   ├── prod.ts             # 生产配置
+│   └── index.ts            # 通用配置
+├── .env.development        # 开发环境变量
+├── .env.production         # 生产环境变量
+└── package.json
+```
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+cd mini
+pnpm install
+```
+
+### 2. 配置环境变量
+
+编辑 `.env.development` 和 `.env.production` 文件,设置API地址:
+
+```bash
+API_BASE_URL=http://localhost:3000
+API_VERSION=v1
+```
+
+### 3. 启动开发服务器
+
+```bash
+# 微信小程序
+npm run dev:weapp
+
+# H5
+npm run dev:h5
+```
+
+### 4. 构建生产版本
+
+```bash
+# 微信小程序
+npm run build:weapp
+
+# H5
+npm run build:h5
+```
+
+## API接口
+
+### 认证相关
+- POST `/api/v1/auth/login` - 密码登录
+- POST `/api/v1/auth/register` - 用户注册
+- GET `/api/v1/auth/me` - 获取当前用户信息
+- POST `/api/v1/auth/logout` - 退出登录
+
+### 用户相关
+- GET `/api/v1/users` - 获取用户列表
+- GET `/api/v1/users/:id` - 获取单个用户
+- PUT `/api/v1/users/:id` - 更新用户信息
+- DELETE `/api/v1/users/:id` - 删除用户
+
+## 使用说明
+
+### 认证状态管理
+
+项目提供了完整的认证状态管理工具:
+
+```typescript
+import { authManager } from '@/utils/auth'
+
+// 检查登录状态
+const isLoggedIn = authManager.isLoggedIn()
+
+// 获取用户信息
+const user = authManager.getUserInfo()
+
+// 获取token
+const token = authManager.getToken()
+
+// 登录
+const user = await authManager.login('username', 'password')
+
+// 注册
+const user = await authManager.register({
+  username: 'newuser',
+  password: 'password123',
+  email: 'user@example.com'
+})
+
+// 退出
+await authManager.logout()
+```
+
+### API调用
+
+使用封装的API客户端进行网络请求:
+
+```typescript
+import { authClient } from '@/api'
+
+// 登录
+const response = await authClient.login.$post({
+  json: {
+    username: 'username',
+    password: 'password'
+  }
+})
+
+// 获取用户列表
+const users = await userClient.$get({})
+
+// 更新用户信息
+const updated = await userClient[':id'].$put({
+  param: { id: 1 },
+  json: { username: 'newname' }
+})
+```
+
+## 环境配置
+
+### 开发环境
+- API地址: `http://localhost:3000`
+- 环境变量文件: `.env.development`
+
+### 生产环境
+- API地址: `https://your-domain.com`
+- 环境变量文件: `.env.production`
+
+## 注意事项
+
+1. **CORS配置**: 确保后端API已配置CORS,允许小程序域名访问
+2. **HTTPS**: 生产环境必须使用HTTPS
+3. **域名配置**: 微信小程序需要在后台配置request合法域名
+4. **存储限制**: 小程序本地存储有大小限制,避免存储过多数据
+
+## 常见问题
+
+### 1. 网络请求失败
+- 检查API地址配置是否正确
+- 确保后端服务已启动
+- 检查网络连接
+
+### 2. 跨域问题
+- 在后端配置CORS
+- 使用代理服务器(开发环境)
+
+### 3. 登录状态丢失
+- 检查token是否正确存储
+- 确认token有效期
+
+## 扩展建议
+
+1. **添加微信登录**: 集成微信OAuth
+2. **手机号登录**: 添加短信验证码功能
+3. **第三方登录**: 支持QQ、微博等
+4. **用户头像**: 添加头像上传功能
+5. **用户权限**: 实现角色权限管理
+
+## 许可证
+
+MIT License

+ 17 - 0
mini-talent/babel.config.js

@@ -0,0 +1,17 @@
+// babel-preset-taro 更多选项和默认值:
+// https://docs.taro.zone/docs/next/babel-config
+module.exports = {
+  presets: [
+    ['taro', {
+      framework: 'react',
+      ts: true,
+      compiler: 'webpack5',
+      targets: {  
+        ios: '8',  
+        android: '4.1'  
+      },  
+      forceAllTransforms: true,
+      useBuiltIns: process.env.TARO_ENV === 'h5' ? 'usage' : false
+    }]
+  ]
+}

+ 1 - 0
mini-talent/commitlint.config.mjs

@@ -0,0 +1 @@
+export default { extends: ["@commitlint/config-conventional"] };

+ 31 - 0
mini-talent/config/dev.ts

@@ -0,0 +1,31 @@
+import type { UserConfigExport } from "@tarojs/cli"
+
+export default {
+  logger: {
+    quiet: false,
+    stats: true
+  },
+  mini: {},
+  h5: {
+    devServer: {
+      // 配置 HMR WebSocket 端口
+      client: {
+        progress: true,
+        webSocketURL: {
+          pathname: '/mini-ws',
+          port: 443, // 指定 HMR WebSocket 端口
+        },
+      },
+      open: false
+    },
+    webpackChain(chain, webpack) {  
+      // 确保在 HtmlWebpackPlugin 之后添加  
+      chain  
+        .plugin('iframeCommunicationPlugin')  
+        .use(require('webpack-plugin-iframe-communicator').default, [{  
+          hostOrigin: '*',
+        }])  
+        .after('htmlWebpackPlugin'); // 指定在 htmlWebpackPlugin 之后执行  
+    }
+  }
+} satisfies UserConfigExport<'webpack5'>

+ 143 - 0
mini-talent/config/index.ts

@@ -0,0 +1,143 @@
+import { defineConfig, type UserConfigExport } from '@tarojs/cli'
+import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
+import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack'
+import devConfig from './dev'
+import prodConfig from './prod'
+
+// 获取当前编译平台(默认微信小程序)
+const getPlatform = () => {
+  // 从环境变量获取编译平台,Taro 会自动设置TARO_ENV 环境变量
+  return process.env.TARO_ENV || 'weapp'
+}
+
+// 获取当前环境(开发/生产)
+const getEnv = () => {
+  return process.env.NODE_ENV || 'development'
+}
+
+export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
+  // 动态生成输出目录:dist/平台/环境
+  const platform = getPlatform()
+  const env = getEnv()
+  const outputDir = `dist/${platform}/${env}`
+
+  const baseConfig: UserConfigExport<'webpack5'> = {
+    projectName: 'mini',
+    date: '2025-7-27',
+    designWidth: 750,
+    deviceRatio: {
+      640: 2.34 / 2,
+      750: 1,
+      375: 2,
+      828: 1.81 / 2
+    },
+    sourceRoot: 'src',
+    outputRoot: outputDir, // 使用动态生成的输出目录
+    plugins: [
+      "@tarojs/plugin-generator"
+    ],
+    defineConstants: {
+    },
+    copy: {
+      patterns: [
+      ],
+      options: {
+      }
+    },
+    framework: 'react',
+    compiler: 'webpack5',
+    cache: {
+      enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
+    },
+    mini: {
+      postcss: {
+        pxtransform: {
+          enable: true,
+          config: {
+
+          }
+        },
+        cssModules: {
+          enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+          config: {
+            namingPattern: 'module', // 转换模式,取值为 global/module
+            generateScopedName: '[name]__[local]___[hash:base64:5]'
+          }
+        }
+      },
+      webpackChain(chain) {
+        chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
+        chain.merge({
+          plugin: {
+            install: {
+              plugin: UnifiedWebpackPluginV5,
+              args: [{
+                // 这里可以传参数
+                cssSelectorReplacement:{
+                  universal: ['view','text','button', 'input']
+                },
+                cssChildCombinatorReplaceValue: ['view', 'text', 'button', 'input']  
+              }]
+            }
+          }
+        })
+      }
+    },
+    h5: {
+      publicPath: '/mini/',
+      staticDirectory: 'static',
+      output: {
+        filename: 'js/[name].[hash:8].js',
+        chunkFilename: 'js/[name].[chunkhash:8].js'
+      },
+      miniCssExtractPluginOption: {
+        ignoreOrder: true,
+        filename: 'css/[name].[hash].css',
+        chunkFilename: 'css/[name].[chunkhash].css'
+      },
+      postcss: {
+        pxtransform: {
+          enable: true,
+          config: {
+            baseFontSize : 14,
+            minRootSize: 14
+          }
+        },
+        autoprefixer: {
+          enable: true,
+          config: {}
+        },
+        cssModules: {
+          enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+          config: {
+            namingPattern: 'module', // 转换模式,取值为 global/module
+            generateScopedName: '[name]__[local]___[hash:base64:5]'
+          }
+        }
+      },
+      webpackChain(chain) {
+        chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
+      },
+      router: {
+        basename: '/mini',
+      },
+    },
+    rn: {
+      appName: 'taroDemo',
+      postcss: {
+        cssModules: {
+          enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+        }
+      }
+    }
+  }
+
+  process.env.BROWSERSLIST_ENV = process.env.NODE_ENV
+
+  if (process.env.NODE_ENV === 'development') {
+    // 本地开发构建配置(不混淆压缩)
+    return merge({}, baseConfig, devConfig)
+  }
+  // 生产构建配置(默认开启压缩混淆等)
+  return merge({}, baseConfig, prodConfig)
+})

+ 39 - 0
mini-talent/config/prod.ts

@@ -0,0 +1,39 @@
+import type { UserConfigExport } from "@tarojs/cli"
+
+export default {
+  mini: {},
+  h5: {
+    compile: {
+      include: [
+        // 确保产物为 es5
+        filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename)
+      ]
+    },
+    /**
+     * WebpackChain 插件配置
+     * @docs https://github.com/neutrinojs/webpack-chain
+     */
+    // webpackChain (chain) {
+    //   /**
+    //    * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
+    //    * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
+    //    */
+    //   chain.plugin('analyzer')
+    //     .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
+    //   /**
+    //    * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
+    //    * @docs https://github.com/chrisvfritz/prerender-spa-plugin
+    //    */
+    //   const path = require('path')
+    //   const Prerender = require('prerender-spa-plugin')
+    //   const staticDir = path.join(__dirname, '..', 'dist')
+    //   chain
+    //     .plugin('prerender')
+    //     .use(new Prerender({
+    //       staticDir,
+    //       routes: [ '/pages/index/index' ],
+    //       postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
+    //     }))
+    // }
+  }
+} satisfies UserConfigExport<'webpack5'>

+ 38 - 0
mini-talent/jest.config.js

@@ -0,0 +1,38 @@
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'jsdom',
+  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1',
+    '^~/(.*)$': '<rootDir>/tests/$1',
+    '^@tarojs/taro$': '<rootDir>/tests/__mocks__/taroMock.ts',
+    '\.(css|less|scss|sass)$': '<rootDir>/tests/__mocks__/styleMock.js',
+    '\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
+      '<rootDir>/tests/__mocks__/fileMock.js'
+  },
+  testMatch: [
+    '<rootDir>/tests/**/*.spec.{ts,tsx}',
+    '<rootDir>/tests/**/*.test.{ts,tsx}'
+  ],
+  collectCoverageFrom: [
+    'src/**/*.{ts,tsx}',
+    '!src/**/*.d.ts',
+    '!src/**/index.{ts,tsx}',
+    '!src/**/*.stories.{ts,tsx}'
+  ],
+  coverageDirectory: 'coverage',
+  coverageReporters: ['text', 'lcov', 'html'],
+  testPathIgnorePatterns: [
+    '/node_modules/',
+    '/dist/',
+    '/coverage/'
+  ],
+  transform: {
+    '^.+\\.(ts|tsx)$': 'babel-jest',
+    '^.+\\.(js|jsx)$': 'babel-jest'
+  },
+  transformIgnorePatterns: [
+    '/node_modules/(?!(swiper|@tarojs)/)'
+  ],
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
+}

+ 135 - 0
mini-talent/package.json

@@ -0,0 +1,135 @@
+{
+  "name": "mini",
+  "version": "1.0.0",
+  "private": true,
+  "description": "",
+  "templateInfo": {
+    "name": "default",
+    "typescript": true,
+    "css": "None",
+    "framework": "React"
+  },
+  "scripts": {
+    "prepare": "husky",
+    "postinstall": "weapp-tw patch",
+    "new": "taro new",
+    "build:weapp": "taro build --type weapp",
+    "build:swan": "taro build --type swan",
+    "build:alipay": "taro build --type alipay",
+    "build:tt": "taro build --type tt",
+    "build:h5": "taro build --type h5",
+    "build:rn": "taro build --type rn",
+    "build:qq": "taro build --type qq",
+    "build:jd": "taro build --type jd",
+    "build:harmony-hybrid": "taro build --type harmony-hybrid",
+    "dev:weapp": "npm run build:weapp -- --watch",
+    "dev:swan": "npm run build:swan -- --watch",
+    "dev:alipay": "npm run build:alipay -- --watch",
+    "dev:tt": "npm run build:tt -- --watch",
+    "dev:h5": "npm run build:h5 -- --watch",
+    "dev:rn": "npm run build:rn -- --watch",
+    "dev:qq": "npm run build:qq -- --watch",
+    "dev:jd": "npm run build:jd -- --watch",
+    "dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch",
+    "test": "jest",
+    "test:watch": "jest --watch",
+    "test:coverage": "jest --coverage",
+    "test:components": "jest tests/components",
+    "test:pages": "jest tests/pages",
+    "typecheck": "tsc --noEmit"
+  },
+  "browserslist": {
+    "development": [
+      "defaults and fully supports es6-module",
+      "maintained node versions"
+    ],
+    "production": [
+      "last 3 versions",
+      "Android >= 4.1",
+      "ios >= 8"
+    ]
+  },
+  "author": "",
+  "dependencies": {
+    "@babel/runtime": "^7.24.4",
+    "@d8d/mini-shared-ui-components": "workspace:*",
+    "@d8d/mini-enterprise-auth-ui": "workspace:*",
+    "@d8d/yongren-dashboard-ui": "workspace:*",
+    "@d8d/yongren-order-management-ui": "workspace:*",
+    "@d8d/yongren-talent-management-ui": "workspace:*",
+    "@d8d/yongren-statistics-ui": "workspace:*",
+    "@d8d/yongren-settings-ui": "workspace:*",
+    "@d8d/yongren-shared-ui": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@radix-ui/react-slot": "^1.2.3",
+    "@tanstack/react-query": "^5.90.12",
+    "@tarojs/components": "4.1.4",
+    "@tarojs/helper": "4.1.4",
+    "@tarojs/plugin-framework-react": "4.1.4",
+    "@tarojs/plugin-platform-alipay": "4.1.4",
+    "@tarojs/plugin-platform-h5": "4.1.4",
+    "@tarojs/plugin-platform-harmony-hybrid": "4.1.4",
+    "@tarojs/plugin-platform-jd": "4.1.4",
+    "@tarojs/plugin-platform-qq": "4.1.4",
+    "@tarojs/plugin-platform-swan": "4.1.4",
+    "@tarojs/plugin-platform-tt": "4.1.4",
+    "@tarojs/plugin-platform-weapp": "4.1.4",
+    "@tarojs/react": "4.1.4",
+    "@tarojs/runtime": "4.1.4",
+    "@tarojs/shared": "4.1.4",
+    "@tarojs/taro": "4.1.4",
+    "@weapp-tailwindcss/merge": "^1.2.3",
+    "abortcontroller-polyfill": "^1.7.8",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "hono": "4.8.5",
+    "react": "^18.0.0",
+    "react-dom": "^18.0.0",
+    "react-hook-form": "^7.62.0",
+    "zod": "^4.0.14"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.24.4",
+    "@babel/plugin-transform-class-properties": "7.25.9",
+    "@babel/preset-react": "^7.24.1",
+    "@commitlint/cli": "^19.8.1",
+    "@commitlint/config-conventional": "^19.8.1",
+    "@egoist/tailwindcss-icons": "^1.9.0",
+    "@iconify/json": "^2.2.365",
+    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
+    "@tailwindcss/postcss": "^4.1.11",
+    "@tarojs/cli": "4.1.4",
+    "@tarojs/plugin-generator": "4.1.4",
+    "@tarojs/taro-loader": "4.1.4",
+    "@tarojs/webpack5-runner": "4.1.4",
+    "@testing-library/jest-dom": "^6.8.0",
+    "@testing-library/react": "^16.3.0",
+    "@testing-library/user-event": "^14.6.1",
+    "@types/jest": "^29.5.14",
+    "@types/node": "^18",
+    "@types/react": "^18.0.0",
+    "@types/webpack-env": "^1.13.6",
+    "autoprefixer": "^10.4.21",
+    "babel-preset-taro": "4.1.4",
+    "eslint": "^8.57.0",
+    "eslint-config-taro": "4.1.4",
+    "eslint-plugin-react": "^7.34.1",
+    "eslint-plugin-react-hooks": "^4.4.0",
+    "html-webpack-plugin": "^5.6.3",
+    "husky": "^9.1.7",
+    "jest": "^30.2.0",
+    "jest-environment-jsdom": "^29.7.0",
+    "lint-staged": "^16.1.2",
+    "postcss": "^8.4.38",
+    "react-refresh": "^0.14.0",
+    "stylelint": "^16.4.0",
+    "stylelint-config-standard": "^38.0.0",
+    "tailwindcss": "^4.1.11",
+    "ts-jest": "^29.4.5",
+    "tsconfig-paths-webpack-plugin": "^4.1.0",
+    "typescript": "^5.4.5",
+    "weapp-tailwindcss": "^4.2.5",
+    "webpack": "5.91.0",
+    "webpack-plugin-iframe-communicator": "^0.0.10"
+  }
+}

+ 5 - 0
mini-talent/postcss.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  plugins: {
+    "@tailwindcss/postcss": {},
+  }
+}

+ 15 - 0
mini-talent/project.config.json

@@ -0,0 +1,15 @@
+{
+  "miniprogramRoot": "./dist",
+  "projectname": "mini",
+  "description": "",
+  "appid": "wx6765e6b2fe06378c",
+  "setting": {
+    "urlCheck": false,
+    "es6": false,
+    "enhance": false,
+    "compileHotReLoad": false,
+    "postcss": false,
+    "minified": true
+  },
+  "compileType": "miniprogram"
+}

+ 35 - 0
mini-talent/src/api.ts

@@ -0,0 +1,35 @@
+import type {
+  AuthRoutes,
+  UserRoutes,
+  RoleRoutes,
+  FileRoutes,
+  ChannelRoutes,
+  CompanyRoutes,
+  DisabilityRoutes,
+  OrderRoutes,
+  PlatformRoutes,
+  SalaryRoutes,
+  EnterpriseAuthRoutes,
+  EnterpriseCompanyRoutes,
+  EnterpriseDisabilityRoutes
+} from '@d8d/server'
+import { rpcClient } from './utils/rpc-client'
+
+// 创建各个模块的RPC客户端
+export const authClient = rpcClient<AuthRoutes>().api.v1.auth
+export const userClient = rpcClient<UserRoutes>().api.v1.users
+export const roleClient = rpcClient<RoleRoutes>().api.v1.roles
+export const fileClient = rpcClient<FileRoutes>().api.v1.files
+
+// Allin系统移植模块客户端
+export const channelClient = rpcClient<ChannelRoutes>().api.v1.channel
+export const companyClient = rpcClient<CompanyRoutes>().api.v1.company
+export const disabilityClient = rpcClient<DisabilityRoutes>().api.v1.disability
+export const orderClient = rpcClient<OrderRoutes>().api.v1.order
+export const platformClient = rpcClient<PlatformRoutes>().api.v1.platform
+export const salaryClient = rpcClient<SalaryRoutes>().api.v1.salary
+
+// 用人方小程序专用客户端(史诗012补充API)
+export const enterpriseAuthClient = rpcClient<EnterpriseAuthRoutes>().api.v1.yongren.auth
+export const enterpriseCompanyClient = rpcClient<EnterpriseCompanyRoutes>().api.v1.yongren.company
+export const enterpriseDisabilityClient = rpcClient<EnterpriseDisabilityRoutes>().api.v1.yongren['disability-person']

+ 51 - 0
mini-talent/src/app.config.ts

@@ -0,0 +1,51 @@
+export default defineAppConfig({
+  pages: [
+    'pages/yongren/dashboard/index',
+    'pages/yongren/talent/list/index',
+    'pages/yongren/talent/detail/index',
+    'pages/yongren/order/list/index',
+    'pages/yongren/order/detail/index',
+    'pages/yongren/statistics/index',
+    'pages/yongren/settings/index',
+    'pages/yongren/video/index',
+    // 原有小程序页面(企业用户专用)
+    'pages/profile/index',
+    'pages/login/index'
+  ],
+  window: {
+    backgroundTextStyle: 'light',
+    navigationBarBackgroundColor: '#3b82f6',
+    navigationBarTitleText: '用人方小程序',
+    navigationBarTextStyle: 'white',
+    navigationStyle: 'custom'
+  },
+  tabBar: {
+    custom: true,
+    color: "#6b7280",
+    selectedColor: "#3b82f6",
+    backgroundColor: "#ffffff",
+    list: [
+      {
+        pagePath: 'pages/yongren/dashboard/index',
+        text: '首页'
+      },
+      {
+        pagePath: 'pages/yongren/talent/list/index',
+        text: '人才'
+      },
+      {
+        pagePath: 'pages/yongren/order/list/index',
+        text: '订单'
+      },
+      {
+        pagePath: 'pages/yongren/statistics/index',
+        text: '数据'
+      },
+      {
+        pagePath: 'pages/yongren/settings/index',
+        text: '设置'
+      }
+    ]
+  },
+  usingComponents: {}
+})

+ 193 - 0
mini-talent/src/app.css

@@ -0,0 +1,193 @@
+@import "weapp-tailwindcss";
+@config "../tailwind.config.js";
+
+/* 小程序滚动条样式规范 */
+/* 基于微信小程序设计规范,适配iOS和Android双平台 */
+
+/* 全局滚动条样式 */
+::-webkit-scrollbar {
+  width: 6rpx;
+  height: 6rpx;
+}
+
+::-webkit-scrollbar-track {
+  background-color: transparent;
+  border-radius: 3rpx;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: rgba(0, 0, 0, 0.2);
+  border-radius: 3rpx;
+  transition: background-color 0.2s ease;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background-color: rgba(0, 0, 0, 0.3);
+}
+
+::-webkit-scrollbar-thumb:active {
+  background-color: rgba(0, 0, 0, 0.4);
+}
+
+/* 深色模式下的滚动条样式 */
+@media (prefers-color-scheme: dark) {
+  ::-webkit-scrollbar-thumb {
+    background-color: rgba(255, 255, 255, 0.3);
+  }
+
+  ::-webkit-scrollbar-thumb:hover {
+    background-color: rgba(255, 255, 255, 0.4);
+  }
+
+  ::-webkit-scrollbar-thumb:active {
+    background-color: rgba(255, 255, 255, 0.5);
+  }
+}
+
+/* 页面滚动容器样式 */
+.scroll-container {
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
+}
+
+/* 横向滚动容器样式 */
+.scroll-horizontal {
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+  white-space: nowrap;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
+}
+
+/* 卡片滚动样式 */
+.card-scroll {
+  max-height: 600rpx;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.card-scroll::-webkit-scrollbar {
+  width: 4rpx;
+}
+
+.card-scroll::-webkit-scrollbar-thumb {
+  background-color: rgba(24, 144, 255, 0.3);
+  border-radius: 2rpx;
+}
+
+/* 列表滚动样式 */
+.list-scroll {
+  flex: 1;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.list-scroll::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+}
+
+/* 弹窗滚动样式 */
+.modal-scroll {
+  max-height: 800rpx;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.modal-scroll::-webkit-scrollbar {
+  width: 4rpx;
+}
+
+.modal-scroll::-webkit-scrollbar-thumb {
+  background-color: rgba(0, 0, 0, 0.15);
+  border-radius: 2rpx;
+}
+
+/* 自定义滚动条类名 */
+.scrollbar-thin {
+  scrollbar-width: thin;
+  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
+}
+
+.scrollbar-thin::-webkit-scrollbar {
+  width: 4rpx;
+  height: 4rpx;
+}
+
+.scrollbar-none {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+.scrollbar-none::-webkit-scrollbar {
+  display: none;
+}
+
+/* 主题色滚动条 */
+.scrollbar-primary::-webkit-scrollbar-thumb {
+  background-color: rgba(24, 144, 255, 0.5);
+}
+
+.scrollbar-success::-webkit-scrollbar-thumb {
+  background-color: rgba(82, 196, 26, 0.5);
+}
+
+.scrollbar-warning::-webkit-scrollbar-thumb {
+  background-color: rgba(250, 173, 20, 0.5);
+}
+
+.scrollbar-error::-webkit-scrollbar-thumb {
+  background-color: rgba(255, 77, 79, 0.5);
+}
+
+/* 响应式滚动条 */
+@media screen and (max-width: 375px) {
+  ::-webkit-scrollbar {
+    width: 4rpx;
+    height: 4rpx;
+  }
+}
+
+@media screen and (min-width: 768px) {
+  ::-webkit-scrollbar {
+    width: 8rpx;
+    height: 8rpx;
+  }
+}
+
+/* 滚动条动画效果 */
+@keyframes scrollFadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+.scroll-container:hover::-webkit-scrollbar-thumb {
+  animation: scrollFadeIn 0.2s ease;
+}
+
+/* 滚动条指示器 */
+.scroll-indicator {
+  position: relative;
+}
+
+.scroll-indicator::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 2rpx;
+  height: 100%;
+  background: linear-gradient(to bottom, transparent, rgba(24, 144, 255, 0.3), transparent);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.scroll-indicator:hover::after {
+  opacity: 1;
+}

+ 23 - 0
mini-talent/src/app.tsx

@@ -0,0 +1,23 @@
+import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
+import '@/utils/headers-polyfill.js'
+import { PropsWithChildren } from 'react'
+import { useLaunch } from '@tarojs/taro'
+import { QueryClientProvider } from '@tanstack/react-query'
+import { AuthProvider, queryClient } from '@d8d/mini-enterprise-auth-ui/hooks'
+
+import './app.css'
+
+function App({ children }: PropsWithChildren<any>) { 
+  useLaunch(() => {
+    console.log('App launched.')
+  })
+
+  // children 是将要会渲染的页面
+  return (
+    <QueryClientProvider client={queryClient}>
+      <AuthProvider>{children}</AuthProvider>
+    </QueryClientProvider>
+  )
+}
+
+export default App

+ 131 - 0
mini-talent/src/components/ui/avatar-upload.tsx

@@ -0,0 +1,131 @@
+import { useState } from 'react'
+import { View, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { cn } from '@/utils/cn'
+import { uploadFromSelect, type UploadResult } from '@/utils/minio'
+
+interface AvatarUploadProps {
+  currentAvatar?: string
+  onUploadSuccess?: (result: UploadResult) => void
+  onUploadError?: (error: Error) => void
+  size?: number
+  editable?: boolean
+}
+
+export function AvatarUpload({ 
+  currentAvatar, 
+  onUploadSuccess, 
+  onUploadError,
+  size = 96,
+  editable = true 
+}: AvatarUploadProps) {
+  const [uploading, setUploading] = useState(false)
+  const [progress, setProgress] = useState(0)
+
+  const handleChooseImage = async () => {
+    if (!editable || uploading) return
+
+    try {
+      setUploading(true)
+      setProgress(0)
+
+      const result = await uploadFromSelect(
+        'avatars',
+        {
+          sourceType: ['album', 'camera'],
+          count: 1
+        },
+        {
+          onProgress: (event) => {
+            setProgress(event.progress)
+            if (event.stage === 'uploading') {
+              Taro.showLoading({
+                title: `上传中...${event.progress}%`
+              })
+            }
+          },
+          onComplete: () => {
+            Taro.hideLoading()
+            Taro.showToast({
+              title: '上传成功',
+              icon: 'success'
+            })
+          },
+          onError: (error) => {
+            Taro.hideLoading()
+            onUploadError?.(error)
+            Taro.showToast({
+              title: '上传失败',
+              icon: 'none'
+            })
+          }
+        }
+      )
+
+      onUploadSuccess?.(result)
+    } catch (error) {
+      console.error('头像上传失败:', error)
+      onUploadError?.(error as Error)
+    } finally {
+      setUploading(false)
+      setProgress(0)
+    }
+  }
+
+  const avatarSize = size
+
+  return (
+    <View 
+      className="relative inline-block"
+      onClick={handleChooseImage}
+    >
+      <View 
+        className={cn(
+          "relative overflow-hidden rounded-full",
+          "border-4 border-white shadow-lg",
+          editable && "cursor-pointer active:scale-95 transition-transform duration-150",
+          uploading && "opacity-75"
+        )}
+        style={{ width: avatarSize, height: avatarSize }}
+      >
+        <Image
+          src={currentAvatar || 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=160&h=160&fit=crop&crop=face'}
+          mode="aspectFill"
+          className="w-full h-full"
+        />
+        
+        {uploading && (
+          <View className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
+            <View className="text-white text-xs">{progress}%</View>
+          </View>
+        )}
+      </View>
+
+      {editable && !uploading && (
+        <View 
+          className={cn(
+            "absolute -bottom-1 -right-1",
+            "w-8 h-8 bg-blue-500 rounded-full",
+            "flex items-center justify-center shadow-md",
+            "border-2 border-white"
+          )}
+        >
+          <View className="i-heroicons-camera-20-solid w-4 h-4 text-white" />
+        </View>
+      )}
+
+      {uploading && (
+        <View 
+          className={cn(
+            "absolute -bottom-1 -right-1",
+            "w-8 h-8 bg-gray-500 rounded-full",
+            "flex items-center justify-center shadow-md",
+            "border-2 border-white"
+          )}
+        >
+          <View className="i-heroicons-arrow-path-20-solid w-4 h-4 text-white animate-spin" />
+        </View>
+      )}
+    </View>
+  )
+}

+ 46 - 0
mini-talent/src/components/ui/button.tsx

@@ -0,0 +1,46 @@
+import { Button as TaroButton, ButtonProps as TaroButtonProps } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+const buttonVariants = cva(
+  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
+  {
+    variants: {
+      variant: {
+        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+        outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
+        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+        ghost: 'hover:bg-accent hover:text-accent-foreground',
+        link: 'underline-offset-4 hover:underline text-primary',
+      },
+      size: {
+        default: 'h-10 py-2 px-4',
+        sm: 'h-9 px-3 rounded-md text-xs',
+        lg: 'h-11 px-8 rounded-md',
+        icon: 'h-10 w-10',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+interface ButtonProps extends Omit<TaroButtonProps, 'size'>, VariantProps<typeof buttonVariants> {
+  className?: string
+  children?: React.ReactNode
+}
+
+export function Button({ className, variant, size, ...props }: ButtonProps) {
+  return (
+    <TaroButton
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+
+// 预定义的按钮样式导出
+export { buttonVariants }

+ 54 - 0
mini-talent/src/components/ui/card.tsx

@@ -0,0 +1,54 @@
+import { View } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+
+interface CardProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function Card({ className, children }: CardProps) {
+  return (
+    <View className={cn("bg-white rounded-xl shadow-sm", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface CardHeaderProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardHeader({ className, children }: CardHeaderProps) {
+  return (
+    <View className={cn("p-4 border-b border-gray-100", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface CardContentProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardContent({ className, children }: CardContentProps) {
+  return (
+    <View className={cn("p-4", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface CardFooterProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardFooter({ className, children }: CardFooterProps) {
+  return (
+    <View className={cn("p-4 border-t border-gray-100", className)}>
+      {children}
+    </View>
+  )
+}

+ 95 - 0
mini-talent/src/components/ui/dialog.tsx

@@ -0,0 +1,95 @@
+import { useEffect } from 'react'
+import { View, Text } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+
+interface DialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  children: React.ReactNode
+}
+
+export function Dialog({ open, onOpenChange, children }: DialogProps) {
+  useEffect(() => {
+    if (open) {
+      // 在 Taro 中,我们可以使用模态框或者自定义弹窗
+      // 这里使用自定义实现
+    }
+  }, [open])
+
+  const handleBackdropClick = () => {
+    onOpenChange(false)
+  }
+
+  const handleContentClick = (e: any) => {
+    // 阻止事件冒泡,避免点击内容区域时关闭弹窗
+    e.stopPropagation()
+  }
+
+  if (!open) return null
+
+  return (
+    <View
+      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
+      onClick={handleBackdropClick}
+    >
+      <View
+        className="relative bg-white rounded-lg shadow-lg max-w-md w-full mx-4"
+        onClick={handleContentClick}
+      >
+        {children}
+      </View>
+    </View>
+  )
+}
+
+interface DialogContentProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogContent({ className, children }: DialogContentProps) {
+  return (
+    <View className={cn("p-6", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface DialogHeaderProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogHeader({ className, children }: DialogHeaderProps) {
+  return (
+    <View className={cn("mb-4", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface DialogTitleProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogTitle({ className, children }: DialogTitleProps) {
+  return (
+    <Text className={cn("text-lg font-semibold text-gray-900", className)}>
+      {children}
+    </Text>
+  )
+}
+
+interface DialogFooterProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogFooter({ className, children }: DialogFooterProps) {
+  return (
+    <View className={cn("flex justify-end space-x-2", className)}>
+      {children}
+    </View>
+  )
+}

+ 168 - 0
mini-talent/src/components/ui/form.tsx

@@ -0,0 +1,168 @@
+import * as React from "react"
+import { View, Text } from "@tarojs/components"
+import { Slot } from "@radix-ui/react-slot"
+import {
+  Controller,
+  FormProvider,
+  useFormContext,
+  useFormState,
+  type ControllerProps,
+  type FieldPath,
+  type FieldValues,
+} from "react-hook-form"
+
+import { cn } from '@/utils/cn'
+import { Label } from '@/components/ui/label'
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+> = {
+  name: TName
+}
+
+const FormFieldContext = React.createContext<FormFieldContextValue>(
+  {} as FormFieldContextValue
+)
+
+const FormField = <
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+>({
+  ...props
+}: ControllerProps<TFieldValues, TName, TFieldValues>) => {
+  const ControllerWrapper = (props: any) => (
+    // @ts-ignore
+    <Controller {...props} />
+  )
+  return (
+    <FormFieldContext.Provider value={{ name: props.name }}>
+      <ControllerWrapper {...props} />
+    </FormFieldContext.Provider>
+  )
+}
+
+const useFormField = () => {
+  const fieldContext = React.useContext(FormFieldContext)
+  const itemContext = React.useContext(FormItemContext)
+  const { getFieldState } = useFormContext()
+  const formState = useFormState({ name: fieldContext.name })
+  const fieldState = getFieldState(fieldContext.name, formState)
+
+  if (!fieldContext) {
+    throw new Error("useFormField should be used within <FormField>")
+  }
+
+  const { id } = itemContext
+
+  return {
+    id,
+    name: fieldContext.name,
+    formItemId: `${id}-form-item`,
+    formDescriptionId: `${id}-form-item-description`,
+    formMessageId: `${id}-form-item-message`,
+    ...fieldState,
+  }
+}
+
+type FormItemContextValue = {
+  id: string
+}
+
+const FormItemContext = React.createContext<FormItemContextValue>(
+  {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<typeof View>) {
+  const id = React.useId()
+
+  return (
+    <FormItemContext.Provider value={{ id }}>
+      <View
+        className={cn("grid gap-2", className)}
+        {...props}
+      />
+    </FormItemContext.Provider>
+  )
+}
+
+function FormLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof Label>) {
+  const { error, formItemId } = useFormField()
+
+  return (
+    <Label
+      data-slot="form-label"
+      data-error={!!error}
+      className={cn("data-[error=true]:text-destructive", className)}
+      htmlFor={formItemId}
+      {...props}
+    />
+  )
+}
+
+function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
+  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+  return (
+    <Slot
+      data-slot="form-control"
+      id={formItemId}
+      aria-describedby={
+        !error
+          ? `${formDescriptionId}`
+          : `${formDescriptionId} ${formMessageId}`
+      }
+      aria-invalid={!!error}
+      {...props}
+    />
+  )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<typeof Text>) {
+  const { formDescriptionId } = useFormField()
+
+  return (
+    <Text
+      data-slot="form-description"
+      id={formDescriptionId}
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<typeof Text>) {
+  const { error, formMessageId } = useFormField()
+  const body = error ? String(error?.message ?? "") : props.children
+
+  if (!body) {
+    return null
+  }
+
+  return (
+    <Text
+      data-slot="form-message"
+      id={formMessageId}
+      className={cn("text-destructive text-sm", className)}
+      {...props}
+    >
+      {body}
+    </Text>
+  )
+}
+
+export {
+  useFormField,
+  Form,
+  FormItem,
+  FormLabel,
+  FormControl,
+  FormDescription,
+  FormMessage,
+  FormField,
+}

+ 127 - 0
mini-talent/src/components/ui/image.tsx

@@ -0,0 +1,127 @@
+import { View, Image as TaroImage, ImageProps as TaroImageProps } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+import { useState } from 'react'
+
+export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
+  /**
+   * 图片地址
+   */
+  src: string
+  /**
+   * 替代文本
+   */
+  alt?: string
+  /**
+   * 图片模式
+   * @default "aspectFill"
+   */
+  mode?: TaroImageProps['mode']
+  /**
+   * 是否懒加载
+   * @default true
+   */
+  lazyLoad?: boolean
+  /**
+   * 是否显示加载占位
+   * @default true
+   */
+  showLoading?: boolean
+  /**
+   * 是否显示错误占位
+   * @default true
+   */
+  showError?: boolean
+  /**
+   * 圆角大小
+   */
+  rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
+  /**
+   * 自定义样式类
+   */
+  className?: string
+  /**
+   * 图片加载失败时的回调
+   */
+  onError?: () => void
+  /**
+   * 图片加载成功的回调
+   */
+  onLoad?: () => void
+}
+
+const roundedMap = {
+  none: '',
+  sm: 'rounded-sm',
+  md: 'rounded-md',
+  lg: 'rounded-lg',
+  xl: 'rounded-xl',
+  full: 'rounded-full'
+}
+
+export function Image({
+  src,
+  alt = '图片',
+  mode = 'aspectFill',
+  lazyLoad = true,
+  showLoading = true,
+  showError = true,
+  rounded = 'none',
+  className,
+  onError,
+  onLoad,
+  ...props
+}: ImageProps) {
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState(false)
+
+  const handleLoad = () => {
+    setLoading(false)
+    setError(false)
+    onLoad?.()
+  }
+
+  const handleError = () => {
+    setLoading(false)
+    setError(true)
+    onError?.()
+  }
+
+  const renderPlaceholder = () => {
+    if (loading && showLoading) {
+      return (
+        <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
+          <View className="i-heroicons-photo-20-solid w-8 h-8 text-gray-400 animate-pulse" />
+        </View>
+      )
+    }
+
+    if (error && showError) {
+      return (
+        <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
+          <View className="i-heroicons-exclamation-triangle-20-solid w-8 h-8 text-gray-400" />
+        </View>
+      )
+    }
+
+    return null
+  }
+
+  return (
+    <View className={cn('relative overflow-hidden', roundedMap[rounded], className)}>
+      <TaroImage
+        src={src}
+        mode={mode}
+        lazyLoad={lazyLoad}
+        onLoad={handleLoad}
+        onError={handleError}
+        className={cn(
+          'w-full h-full',
+          loading && 'opacity-0',
+          !loading && !error && 'opacity-100 transition-opacity duration-300'
+        )}
+        {...props}
+      />
+      {renderPlaceholder()}
+    </View>
+  )
+}

+ 102 - 0
mini-talent/src/components/ui/input.tsx

@@ -0,0 +1,102 @@
+import { Input as TaroInput, InputProps as TaroInputProps, View, Text } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+import { forwardRef } from 'react'
+
+const inputVariants = cva(
+  'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
+  {
+    variants: {
+      variant: {
+        default: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
+        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+        filled: 'border-none bg-gray-50 hover:bg-gray-100',
+      },
+      size: {
+        default: 'h-10 px-3 py-2',
+        sm: 'h-9 px-2 text-sm',
+        lg: 'h-11 px-4 text-lg',
+        icon: 'h-10 w-10',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+export interface InputProps extends Omit<TaroInputProps, 'className' | 'onChange'>, VariantProps<typeof inputVariants> {
+  className?: string
+  leftIcon?: string
+  rightIcon?: string
+  error?: boolean
+  errorMessage?: string
+  onLeftIconClick?: () => void
+  onRightIconClick?: () => void
+  onChange?: (value: string, event: any) => void
+}
+
+const Input = forwardRef<any, InputProps>(
+  ({ className, variant, size, leftIcon, rightIcon, error, errorMessage, onLeftIconClick, onRightIconClick, onChange, ...props }, ref) => {
+    const handleInput = (event: any) => {
+      const value = event.detail.value
+      onChange?.(value, event)
+      
+      // 同时调用原始的onInput(如果提供了)
+      if (props.onInput) {
+        props.onInput(event)
+      }
+    }
+
+    return (
+      <View className="w-full">
+        <View className="relative">
+          {leftIcon && (
+            <View
+              className={cn(
+                "absolute left-3 top-1/2 -translate-y-1/2",
+                onLeftIconClick ? "cursor-pointer" : "pointer-events-none"
+              )}
+              onClick={onLeftIconClick}
+            >
+              <View className={cn('w-5 h-5 text-gray-400', leftIcon)} />
+            </View>
+          )}
+          
+          <TaroInput
+            ref={ref}
+            className={cn(
+              inputVariants({ variant, size, className }),
+              error && 'border-red-500 focus:border-red-500 focus:ring-red-500',
+              leftIcon && 'pl-10',
+              rightIcon && 'pr-10',
+            )}
+            onInput={handleInput}
+            {...props}
+          />
+          
+          {rightIcon && (
+            <View
+              className={cn(
+                "absolute right-3 top-1/2 -translate-y-1/2",
+                onRightIconClick ? "cursor-pointer" : "pointer-events-none"
+              )}
+              onClick={onRightIconClick}
+            >
+              <View className={cn('w-5 h-5 text-gray-400', rightIcon)} />
+            </View>
+          )}
+        </View>
+        
+        {error && errorMessage && (
+          <Text className="mt-1 text-sm text-red-600">{errorMessage}</Text>
+        )}
+      </View>
+    )
+  }
+)
+
+Input.displayName = 'Input'
+
+export { Input, inputVariants }

+ 55 - 0
mini-talent/src/components/ui/label.tsx

@@ -0,0 +1,55 @@
+import { View, Text } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+import { forwardRef } from 'react'
+
+const labelVariants = cva(
+  'text-sm font-medium',
+  {
+    variants: {
+      variant: {
+        default: 'text-gray-900',
+        secondary: 'text-gray-600',
+        destructive: 'text-red-600',
+      },
+      size: {
+        default: 'text-sm',
+        sm: 'text-xs',
+        lg: 'text-base',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+export interface LabelProps {
+  className?: string
+  variant?: VariantProps<typeof labelVariants>['variant']
+  size?: VariantProps<typeof labelVariants>['size']
+  children: React.ReactNode
+  required?: boolean
+  htmlFor?: string
+}
+
+const Label = forwardRef<HTMLLabelElement, LabelProps>(
+  ({ className, variant, size, children, required, htmlFor, ...props }, _ref) => {
+    return (
+      <View className="mb-2">
+        <Text
+          className={cn(labelVariants({ variant, size, className }))}
+          {...props}
+        >
+          {children}
+          {required && <Text className="text-red-500 ml-1">*</Text>}
+        </Text>
+      </View>
+    )
+  }
+)
+
+Label.displayName = 'Label'
+
+export { Label, labelVariants }

+ 230 - 0
mini-talent/src/components/ui/navbar.tsx

@@ -0,0 +1,230 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+import Taro from '@tarojs/taro'
+import { isWeapp } from '@/utils/platform'
+
+export interface NavbarProps {
+  title?: string
+  leftText?: string
+  leftIcon?: string
+  rightText?: string
+  rightIcon?: string
+  backgroundColor?: string
+  textColor?: string
+  border?: boolean
+  fixed?: boolean
+  placeholder?: boolean
+  onClickLeft?: () => void
+  onClickRight?: () => void
+  children?: React.ReactNode
+  className?: string
+  /** 是否在小程序环境下隐藏右侧按钮(默认false,会自动避让) */
+  hideRightInWeapp?: boolean
+}
+
+const systemInfo = Taro.getSystemInfoSync()
+const menuButtonInfo = isWeapp() ? Taro.getMenuButtonBoundingClientRect() : undefined
+
+// 计算导航栏高度
+const NAVBAR_HEIGHT = 44
+const STATUS_BAR_HEIGHT = systemInfo.statusBarHeight || 0
+const TOTAL_HEIGHT = STATUS_BAR_HEIGHT + NAVBAR_HEIGHT
+
+export const Navbar: React.FC<NavbarProps> = ({
+  title,
+  leftText,
+  leftIcon = 'i-heroicons-chevron-left-20-solid',
+  rightText,
+  rightIcon,
+  backgroundColor = 'bg-white',
+  textColor = 'text-gray-900',
+  border = true,
+  fixed = true,
+  placeholder = true,
+  onClickLeft,
+  onClickRight,
+  children,
+  className,
+  hideRightInWeapp,
+}) => {
+  // 处理左侧点击
+  const handleLeftClick = () => {
+    if (onClickLeft) {
+      onClickLeft()
+    } else {
+      // 默认返回上一页
+      Taro.navigateBack()
+    }
+  }
+
+  // 渲染左侧内容
+  const renderLeft = () => {
+    if (children) return null
+    
+    return (
+      <View 
+        className="absolute left-3 top-0 bottom-0 flex items-center z-10"
+        style={{ height: NAVBAR_HEIGHT }}
+        onClick={handleLeftClick}
+      >
+        <View className="flex items-center">
+          {leftIcon && (
+            <View className={cn(leftIcon, 'w-5 h-5', textColor)} />
+          )}
+          {leftText && (
+            <Text className={cn('ml-1 text-sm', textColor)}>{leftText}</Text>
+          )}
+        </View>
+      </View>
+    )
+  }
+
+  // 渲染右侧内容
+  const renderRight = () => {
+    if (!rightText && !rightIcon || (hideRightInWeapp && isWeapp())) return null
+        
+    if (isWeapp() && menuButtonInfo) {
+      // 小程序环境下,调整右侧按钮位置
+      return (
+        <View
+          className="absolute top-0 bottom-0 flex items-center z-10"
+          style={{
+            height: NAVBAR_HEIGHT,
+            right: `${systemInfo.screenWidth - menuButtonInfo.left + 10}px`,
+          }}
+          onClick={onClickRight}
+        >
+          <View className="flex items-center">
+            {rightText && (
+              <Text className={cn('mr-1 text-sm', textColor)}>{rightText}</Text>
+            )}
+            {rightIcon && (
+              <View className={cn(rightIcon, 'w-5 h-5', textColor)} />
+            )}
+          </View>
+        </View>
+      )
+    }
+    
+    // H5或其他平台,保持原有样式
+    return (
+      <View
+        className="absolute right-3 top-0 bottom-0 flex items-center z-10"
+        style={{ height: NAVBAR_HEIGHT }}
+        onClick={onClickRight}
+      >
+        <View className="flex items-center">
+          {rightText && (
+            <Text className={cn('mr-1 text-sm', textColor)}>{rightText}</Text>
+          )}
+          {rightIcon && (
+            <View className={cn(rightIcon, 'w-5 h-5', textColor)} />
+          )}
+        </View>
+      </View>
+    )
+  }
+
+  // 渲染标题
+  const renderTitle = () => {
+    if (children) return children
+    
+    if (isWeapp() && menuButtonInfo) {
+      // 小程序环境下,调整标题位置
+      return (
+        <View className="flex-1 flex items-center justify-center">
+          <Text
+            className={cn('text-base font-semibold truncate', textColor)}
+            style={{
+              maxWidth: `calc(100% - ${systemInfo.screenWidth - menuButtonInfo.right + 10}px - 60px - 60px)`
+            }}
+          >
+            {title}
+          </Text>
+        </View>
+      )
+    }
+    
+    // H5或其他平台,保持原有样式
+    return (
+      <Text className={cn('text-base font-semibold', textColor)}>
+        {title}
+      </Text>
+    )
+  }
+
+  // 导航栏样式
+  const navbarStyle = {
+    height: TOTAL_HEIGHT,
+    paddingTop: STATUS_BAR_HEIGHT,
+  }
+
+  return (
+    <>
+      <View
+        className={cn(
+          'relative w-full',
+          backgroundColor,
+          border && 'border-b border-gray-200',
+          fixed && 'fixed top-0 left-0 right-0 z-50',
+          className
+        )}
+        style={navbarStyle}
+      >
+        {/* 导航栏内容 */}
+        <View
+          className="relative flex items-center justify-center"
+          style={{ height: NAVBAR_HEIGHT }}
+        >
+          {renderLeft()}
+          {renderTitle()}
+          {renderRight()}
+        </View>
+      </View>
+      
+      {/* 占位元素 */}
+      {fixed && placeholder && (
+        <View style={{ height: TOTAL_HEIGHT }} />
+      )}
+    </>
+  )
+}
+
+// 预设样式
+export const NavbarPresets = {
+  // 默认白色导航栏
+  default: {
+    backgroundColor: 'bg-white',
+    textColor: 'text-gray-900',
+    border: true,
+  },
+  
+  // 深色导航栏
+  dark: {
+    backgroundColor: 'bg-gray-900',
+    textColor: 'text-white',
+    border: true,
+  },
+  
+  // 透明导航栏
+  transparent: {
+    backgroundColor: 'bg-transparent',
+    textColor: 'text-white',
+    border: false,
+  },
+  
+  // 主色调导航栏
+  primary: {
+    backgroundColor: 'bg-blue-500',
+    textColor: 'text-white',
+    border: false,
+  },
+}
+
+// 快捷创建函数
+export const createNavbar = (preset: keyof typeof NavbarPresets) => {
+  return NavbarPresets[preset]
+}
+
+export default Navbar

+ 37 - 0
mini-talent/src/components/ui/page-container.tsx

@@ -0,0 +1,37 @@
+import React, { ReactNode } from 'react'
+import { View } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+
+export interface PageContainerProps {
+  children: ReactNode
+  className?: string
+  padding?: boolean
+  background?: string
+  safeArea?: boolean
+}
+
+export const PageContainer: React.FC<PageContainerProps> = ({
+  children,
+  className,
+  padding = true,
+  background = 'bg-gray-50',
+  safeArea = true,
+}) => {
+  return (
+    <View className={cn(
+      'min-h-screen w-full',
+      background,
+      safeArea && 'pb-safe',
+      className
+    )}>
+      <View className={cn(
+        padding && 'px-4 py-4',
+        'max-w-screen-md mx-auto'
+      )}>
+        {children}
+      </View>
+    </View>
+  )
+}
+
+export default PageContainer

+ 155 - 0
mini-talent/src/components/ui/tab-bar.tsx

@@ -0,0 +1,155 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import clsx from 'clsx'
+
+export interface TabBarItem {
+  key: string
+  title: string
+  icon?: string
+  selectedIcon?: string
+  iconClass?: string
+  selectedIconClass?: string
+  badge?: number | string
+  dot?: boolean
+}
+
+export interface TabBarProps {
+  items: TabBarItem[]
+  activeKey?: string
+  onChange?: (key: string) => void
+  className?: string
+  style?: React.CSSProperties
+  fixed?: boolean
+  safeArea?: boolean
+  color?: string
+  selectedColor?: string
+  backgroundColor?: string
+}
+
+const TabBar = React.forwardRef<HTMLDivElement, TabBarProps>(({
+  items,
+  activeKey,
+  onChange,
+  className,
+  style,
+  fixed = true,
+  safeArea = true,
+  color = '#7f7f7f',
+  selectedColor = '#1890ff',
+  backgroundColor = '#ffffff',
+}, ref) => {
+  
+  const currentActiveKey = activeKey || items[0]?.key
+
+  const handleTabChange = (key: string) => {
+    if (key !== currentActiveKey) {
+      onChange?.(key)
+    }
+  }
+
+  return (
+    <View
+      ref={ref}
+      className={clsx(
+        'tab-bar',
+        fixed && 'fixed bottom-0 left-0 right-0',
+        safeArea && 'pb-safe',
+        'z-50',
+        className
+      )}
+      style={{
+        backgroundColor,
+        ...style,
+      }}
+    >
+      <View className="flex h-16 border-t border-gray-200">
+        {items.map((item) => {
+          const isActive = item.key === currentActiveKey
+          
+          return (
+            <View
+              key={item.key}
+              className={clsx(
+                'flex-1 flex flex-col items-center justify-center',
+                'px-2 py-1',
+                'cursor-pointer',
+                'transition-colors duration-200',
+                'hover:opacity-80'
+              )}
+              onClick={() => handleTabChange(item.key)}
+            >
+              <View className="relative">
+                {(item.iconClass || item.icon) && (
+                  <View
+                    className={clsx(
+                      'mb-1',
+                      'flex items-center justify-center',
+                      item.iconClass ? 'w-6 h-6' : 'text-2xl',
+                      isActive ? 'text-blue-500' : 'text-gray-500'
+                    )}
+                    style={{
+                      color: isActive ? selectedColor : color,
+                    }}
+                  >
+                    {item.iconClass ? (
+                      <View
+                        className={clsx(
+                          isActive && item.selectedIconClass
+                            ? item.selectedIconClass
+                            : item.iconClass,
+                          'w-full h-full'
+                        )}
+                      />
+                    ) : (
+                      isActive && item.selectedIcon ? item.selectedIcon : item.icon
+                    )}
+                  </View>
+                )}
+                
+                {item.badge && (
+                  <View
+                    className={clsx(
+                      'absolute -top-1 -right-2',
+                      'bg-red-500 text-white text-xs',
+                      'rounded-full px-1.5 py-0.5',
+                      'min-w-4 h-4 flex items-center justify-center'
+                    )}
+                  >
+                    {typeof item.badge === 'number' && item.badge > 99 ? '99+' : item.badge}
+                  </View>
+                )}
+                
+                {item.dot && (
+                  <View
+                    className={clsx(
+                      'absolute -top-1 -right-1',
+                      'w-2 h-2 bg-red-500 rounded-full'
+                    )}
+                  />
+                )}
+              </View>
+              
+              <Text
+                className={clsx(
+                  'text-xs',
+                  'leading-tight',
+                  isActive ? 'font-medium' : 'font-normal'
+                )}
+                style={{
+                  color: isActive ? selectedColor : color,
+                }}
+                numberOfLines={1}
+              >
+                {item.title}
+              </Text>
+            </View>
+          )
+        })}
+      </View>
+    </View>
+  )
+})
+
+TabBar.displayName = 'TabBar'
+
+export { TabBar }

+ 58 - 0
mini-talent/src/components/ui/user-status-bar.tsx

@@ -0,0 +1,58 @@
+import React from 'react'
+import { View, Text, Image } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+
+export interface UserStatusBarProps {
+  userName?: string
+  avatarUrl?: string
+  companyName?: string
+  notificationCount?: number
+  className?: string
+}
+
+export const UserStatusBar: React.FC<UserStatusBarProps> = ({
+  userName = '企业用户',
+  avatarUrl,
+  companyName = '企业名称',
+  notificationCount = 0,
+  className,
+}) => {
+  return (
+    <View className={cn(
+      'flex items-center justify-between px-4 py-3 bg-white border-b border-gray-200',
+      className
+    )}>
+      <View className="flex items-center">
+        {avatarUrl ? (
+          <Image
+            src={avatarUrl}
+            className="w-10 h-10 rounded-full mr-3"
+            mode="aspectFill"
+          />
+        ) : (
+          <View className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center mr-3">
+            <Text className="text-white font-bold text-lg">
+              {userName.charAt(0).toUpperCase()}
+            </Text>
+          </View>
+        )}
+        <View>
+          <Text className="font-semibold text-gray-900">{userName}</Text>
+          <Text className="text-sm text-gray-600">{companyName}</Text>
+        </View>
+      </View>
+      <View className="relative">
+        <View className="i-heroicons-bell-20-solid w-6 h-6 text-gray-600" />
+        {notificationCount > 0 && (
+          <View className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
+            <Text className="text-white text-xs font-bold">
+              {notificationCount > 99 ? '99+' : notificationCount}
+            </Text>
+          </View>
+        )}
+      </View>
+    </View>
+  )
+}
+
+export default UserStatusBar

+ 29 - 0
mini-talent/src/hooks/useRequireAuth.ts

@@ -0,0 +1,29 @@
+import { useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { useAuth } from '@/utils/auth'
+
+/**
+ * 要求认证的hook
+ * 如果用户未登录,则重定向到登录页
+ */
+export const useRequireAuth = () => {
+  const { isLoggedIn, isLoading } = useAuth()
+
+  useEffect(() => {
+    if (!isLoading && !isLoggedIn) {
+      Taro.showToast({
+        title: '请先登录',
+        icon: 'none',
+        duration: 1500
+      })
+
+      setTimeout(() => {
+        Taro.redirectTo({
+          url: '/pages/login/index'
+        })
+      }, 1500)
+    }
+  }, [isLoggedIn, isLoading])
+
+  return { isLoggedIn, isLoading }
+}

+ 31 - 0
mini-talent/src/index.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+  <meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-touch-fullscreen" content="yes">
+  <meta name="format-detection" content="telephone=no,address=no">
+  <meta name="apple-mobile-web-app-status-bar-style" content="white">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
+  <title>mini</title>
+  <script><%= htmlWebpackPlugin.options.script %></script>
+  <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
+  <script>
+    const init = () => {
+      const urlParams = new URLSearchParams(window.location.search);
+      // if ( !urlParams.has('vconsole')) return;
+      var vConsole = new VConsole({
+        theme: urlParams.get('vconsole_theme') || 'light',
+        onReady: function() {
+          console.log('vConsole is ready');
+        }
+      });
+    }
+    init();
+  </script>
+</head>
+<body>
+  <div id="app"></div>
+</body>
+</html>

+ 2 - 0
mini-talent/src/pages/login/index.config.ts

@@ -0,0 +1,2 @@
+// 桥接配置文件:从 @d8d/mini-enterprise-auth-ui 包导入Login配置
+// export { LoginConfig as default } from '@d8d/mini-enterprise-auth-ui'

+ 1 - 0
mini-talent/src/pages/login/index.css

@@ -0,0 +1 @@
+/* 样式已迁移到 @d8d/mini-enterprise-auth-ui 包中的 Login.css 文件 */

+ 3 - 0
mini-talent/src/pages/login/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/mini-enterprise-auth-ui 包导入Login页面
+import Login from '@d8d/mini-enterprise-auth-ui/pages/login/Login'
+export default Login

+ 2 - 0
mini-talent/src/pages/profile/index.config.ts

@@ -0,0 +1,2 @@
+// 桥接配置文件:从 @d8d/mini-enterprise-auth-ui 包导入Profile配置
+// export { ProfileConfig as default } from '@d8d/mini-enterprise-auth-ui'

+ 1 - 0
mini-talent/src/pages/profile/index.css

@@ -0,0 +1 @@
+/* 样式已迁移到 @d8d/mini-enterprise-auth-ui 包中的 Profile.css 文件 */

+ 3 - 0
mini-talent/src/pages/profile/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/mini-enterprise-auth-ui 包导入Profile页面
+import Profile from '@d8d/mini-enterprise-auth-ui/pages/profile/Profile'
+export default Profile

+ 2 - 0
mini-talent/src/pages/yongren/dashboard/index.config.ts

@@ -0,0 +1,2 @@
+// 桥接配置文件:从 @d8d/yongren-dashboard-ui 包导入Dashboard配置
+// export { DashboardConfig as default } from '@d8d/yongren-dashboard-ui'

+ 1 - 0
mini-talent/src/pages/yongren/dashboard/index.css

@@ -0,0 +1 @@
+/* 样式已迁移到 @d8d/yongren-dashboard-ui 包中的 Dashboard.css 文件 */

+ 3 - 0
mini-talent/src/pages/yongren/dashboard/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/yongren-dashboard-ui 包导入Dashboard页面
+import Dashboard from '@d8d/yongren-dashboard-ui/pages/Dashboard/Dashboard'
+export default Dashboard

+ 4 - 0
mini-talent/src/pages/yongren/order/detail/index.config.ts

@@ -0,0 +1,4 @@
+export default {
+  navigationBarTitleText: '订单详情',
+  enablePullDownRefresh: false,
+}

+ 3 - 0
mini-talent/src/pages/yongren/order/detail/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/yongren-order-management-ui 包导入OrderDetail页面
+import OrderDetail from '@d8d/yongren-order-management-ui/pages/OrderDetail/OrderDetail'
+export default OrderDetail

+ 4 - 0
mini-talent/src/pages/yongren/order/list/index.config.ts

@@ -0,0 +1,4 @@
+export default {
+    navigationBarTitleText: '订单列表',
+    enablePullDownRefresh: true,
+  }

+ 3 - 0
mini-talent/src/pages/yongren/order/list/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/yongren-order-management-ui 包导入OrderList页面
+import OrderList from '@d8d/yongren-order-management-ui/pages/OrderList/OrderList'
+export default OrderList

+ 4 - 0
mini-talent/src/pages/yongren/settings/index.config.ts

@@ -0,0 +1,4 @@
+export default {
+    navigationBarTitleText: '设置',
+    enablePullDownRefresh: false,
+  }

+ 3 - 0
mini-talent/src/pages/yongren/settings/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/yongren-settings-ui 包导入Settings页面
+import Settings from '@d8d/yongren-settings-ui/pages/Settings/Settings'
+export default Settings

+ 4 - 0
mini-talent/src/pages/yongren/statistics/index.config.ts

@@ -0,0 +1,4 @@
+export default {
+  navigationBarTitleText: '数据统计',
+  enablePullDownRefresh: false,
+}

+ 3 - 0
mini-talent/src/pages/yongren/statistics/index.tsx

@@ -0,0 +1,3 @@
+// 桥接文件:从 @d8d/yongren-statistics-ui 包导入Statistics页面
+import Statistics from '@d8d/yongren-statistics-ui/pages/Statistics/Statistics'
+export default Statistics

+ 4 - 0
mini-talent/src/pages/yongren/talent/detail/index.config.ts

@@ -0,0 +1,4 @@
+export default {
+  navigationBarTitleText: '人才详情',
+  enablePullDownRefresh: false,
+}

+ 1 - 0
mini-talent/src/pages/yongren/talent/detail/index.css

@@ -0,0 +1 @@
+/* 样式已迁移到 @d8d/yongren-talent-management-ui 包中的 TalentDetail.css 文件 */

+ 5 - 0
mini-talent/src/pages/yongren/talent/detail/index.tsx

@@ -0,0 +1,5 @@
+// 桥接文件:从 @d8d/yongren-talent-management-ui 包导入TalentDetail页面
+import TalentDetail from '@d8d/yongren-talent-management-ui/pages/TalentDetail/TalentDetail'
+import '@d8d/yongren-talent-management-ui/pages/TalentDetail/TalentDetail.css'
+
+export default TalentDetail

+ 4 - 0
mini-talent/src/pages/yongren/talent/list/index.config.ts

@@ -0,0 +1,4 @@
+export default {
+  navigationBarTitleText: '人才列表',
+  enablePullDownRefresh: true,
+}

+ 1 - 0
mini-talent/src/pages/yongren/talent/list/index.css

@@ -0,0 +1 @@
+/* 样式已迁移到 @d8d/yongren-talent-management-ui 包中的 TalentManagement.css 文件 */

+ 4 - 0
mini-talent/src/pages/yongren/talent/list/index.tsx

@@ -0,0 +1,4 @@
+// 桥接文件:从 @d8d/yongren-talent-management-ui 包导入TalentManagement页面
+import TalentManagement from '@d8d/yongren-talent-management-ui/pages/TalentManagement/TalentManagement'
+import '@d8d/yongren-talent-management-ui/pages/TalentManagement/TalentManagement.css'
+export default TalentManagement

+ 4 - 0
mini-talent/src/pages/yongren/video/index.config.ts

@@ -0,0 +1,4 @@
+export default definePageConfig({
+  navigationBarTitleText: '视频管理',
+  navigationStyle: 'custom'
+})

+ 9 - 0
mini-talent/src/pages/yongren/video/index.tsx

@@ -0,0 +1,9 @@
+import React from 'react'
+import { VideoManagement } from '@d8d/yongren-settings-ui'
+import './index.config'
+
+const VideoManagementPage: React.FC = () => {
+  return <VideoManagement />
+}
+
+export default VideoManagementPage

+ 26 - 0
mini-talent/src/schemas/register.schema.ts

@@ -0,0 +1,26 @@
+import { z } from 'zod'
+
+export const registerSchema = z.object({
+  username: z.string()
+    .min(3, '用户名至少3个字符')
+    .max(20, '用户名最多20个字符')
+    .regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
+  
+  email: z.string()
+    .optional()
+    .refine(
+      (val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
+      '请输入有效的邮箱地址'
+    ),
+  
+  password: z.string()
+    .min(6, '密码至少6位')
+    .max(20, '密码最多20位'),
+  
+  confirmPassword: z.string()
+}).refine((data) => data.password === data.confirmPassword, {
+  message: '两次输入的密码不一致',
+  path: ['confirmPassword']
+})
+
+export type RegisterFormData = z.infer<typeof registerSchema>

+ 172 - 0
mini-talent/src/utils/auth.tsx

@@ -0,0 +1,172 @@
+import { createContext, useContext, PropsWithChildren } from 'react'
+import Taro from '@tarojs/taro'
+import { enterpriseAuthClient } from '../api'
+import { InferResponseType, InferRequestType } from 'hono'
+import { QueryClient, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+
+// 用户类型定义 - 使用企业用户认证
+export type User = InferResponseType<typeof enterpriseAuthClient.me.$get, 200>
+type LoginRequest = InferRequestType<typeof enterpriseAuthClient.login.$post>['json']
+// 企业用户注册可能由管理员创建,前端不提供注册接口
+type RegisterRequest = { username: string; password: string }
+
+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)
+
+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: () => {
+      queryClient.setQueryData(['currentUser'], null)
+      Taro.redirectTo({ 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
+}
+
+export { queryClient }

+ 2 - 0
mini-talent/src/utils/cn.ts

@@ -0,0 +1,2 @@
+// 从共享UI组件包重新导出cn函数
+export { cn } from '@d8d/mini-shared-ui-components'

+ 79 - 0
mini-talent/src/utils/headers-polyfill.js

@@ -0,0 +1,79 @@
+class Headers {
+    constructor(init = {}) {
+      this._headers = {};
+  
+      if (init instanceof Headers) {
+        // 如果传入的是另一个 Headers 实例,复制其内容
+        init.forEach((value, name) => {
+          this.append(name, value);
+        });
+      } else if (init) {
+        // 处理普通对象或数组
+        Object.entries(init).forEach(([name, value]) => {
+          if (Array.isArray(value)) {
+            // 处理数组值(如 ['value1', 'value2'])
+            value.forEach(v => this.append(name, v));
+          } else {
+            this.set(name, value);
+          }
+        });
+      }
+    }
+  
+    // 添加头(可重复添加同名头)
+    append(name, value) {
+      const normalizedName = this._normalizeName(name);
+      if (this._headers[normalizedName]) {
+        this._headers[normalizedName] += `, ${value}`;
+      } else {
+        this._headers[normalizedName] = String(value);
+      }
+    }
+  
+    // 设置头(覆盖同名头)
+    set(name, value) {
+      this._headers[this._normalizeName(name)] = String(value);
+    }
+  
+    // 获取头
+    get(name) {
+      return this._headers[this._normalizeName(name)] || null;
+    }
+  
+    // 检查是否存在头
+    has(name) {
+      return this._normalizeName(name) in this._headers;
+    }
+  
+    // 删除头
+    delete(name) {
+      delete this._headers[this._normalizeName(name)];
+    }
+  
+    // 遍历头
+    forEach(callback) {
+      Object.entries(this._headers).forEach(([name, value]) => {
+        callback(value, name, this);
+      });
+    }
+  
+    // 获取所有头(原始对象)
+    raw() {
+      return { ...this._headers };
+    }
+  
+    // 规范化头名称(转为小写)
+    _normalizeName(name) {
+      if (typeof name !== 'string') {
+        throw new TypeError('Header name must be a string');
+      }
+      return name.toLowerCase();
+    }
+  }
+  
+  // 全局注册(如果需要)
+  if (typeof globalThis.Headers === 'undefined') {
+    globalThis.Headers = Headers;
+  }
+  
+  export default Headers;

+ 879 - 0
mini-talent/src/utils/minio.ts

@@ -0,0 +1,879 @@
+import type { InferResponseType } from 'hono/client';
+import { fileClient } from "../api";
+import { isWeapp, isH5 } from './platform';
+import Taro from '@tarojs/taro';
+
+// 平台检测 - 使用统一的 platform.ts
+const isMiniProgram = isWeapp();
+const isBrowser = isH5();
+
+export interface MinioProgressEvent {
+  stage: 'uploading' | 'complete' | 'error';
+  message: string;
+  progress: number;
+  details?: {
+      loaded: number;
+      total: number;
+  };
+  timestamp: number;
+}
+
+export interface MinioProgressCallbacks {
+  onProgress?: (event: MinioProgressEvent) => void;
+  onComplete?: () => void;
+  onError?: (error: Error) => void;
+  signal?: AbortSignal | { aborted: boolean };
+}
+
+export interface UploadResult {
+  fileUrl: string;
+  fileKey: string;
+  bucketName: string;
+  fileId: number;
+}
+
+interface UploadPart {
+  ETag: string;
+  PartNumber: number;
+}
+
+interface UploadProgressDetails {
+  partNumber: number;
+  totalParts: number;
+  partSize: number;
+  totalSize: number;
+  partProgress?: number;
+}
+
+type MinioMultipartUploadPolicy = InferResponseType<typeof fileClient["multipart-policy"]['$post'], 200>
+type MinioUploadPolicy = InferResponseType<typeof fileClient["upload-policy"]['$post'], 200>
+
+const PART_SIZE = 5 * 1024 * 1024; // 每部分5MB
+
+// ==================== H5 实现(保留原有代码) ====================
+export class MinIOXHRMultipartUploader {
+  /**
+   * 使用XHR分段上传文件到MinIO(H5环境)
+   */
+  static async upload(
+    policy: MinioMultipartUploadPolicy,
+    file: File | Blob,
+    key: string,
+    callbacks?: MinioProgressCallbacks
+  ): Promise<UploadResult> {
+    const partSize = PART_SIZE;
+    const totalSize = file.size;
+    const totalParts = Math.ceil(totalSize / partSize);
+    const uploadedParts: UploadPart[] = [];
+    
+    callbacks?.onProgress?.({
+      stage: 'uploading',
+      message: '准备上传文件...',
+      progress: 0,
+      details: {
+        loaded: 0,
+        total: totalSize
+      },
+      timestamp: Date.now()
+    });
+    
+    // 分段上传
+    for (let i = 0; i < totalParts; i++) {
+      const start = i * partSize;
+      const end = Math.min(start + partSize, totalSize);
+      const partBlob = file.slice(start, end);
+      const partNumber = i + 1;
+      
+      try {
+        const etag = await this.uploadPart(
+          policy.partUrls[i],
+          partBlob,
+          callbacks,
+          {
+            partNumber,
+            totalParts,
+            partSize: partBlob.size,
+            totalSize
+          }
+        );
+        
+        uploadedParts.push({
+          ETag: etag,
+          PartNumber: partNumber
+        });
+        
+        // 更新进度
+        const progress = Math.round((end / totalSize) * 100);
+        callbacks?.onProgress?.({
+          stage: 'uploading',
+          message: `上传文件片段 ${partNumber}/${totalParts}`,
+          progress,
+          details: {
+            loaded: end,
+            total: totalSize,
+          },
+          timestamp: Date.now()
+        });
+      } catch (error) {
+        callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
+        throw error;
+      }
+    }
+    
+    // 完成上传
+    try {
+      const result = await this.completeMultipartUpload(policy, key, uploadedParts);
+      
+      callbacks?.onProgress?.({
+        stage: 'complete',
+        message: '文件上传完成',
+        progress: 100,
+        timestamp: Date.now()
+      });
+      
+      callbacks?.onComplete?.();
+      return {
+        fileUrl: `${policy.host}/${key}`,
+        fileKey: key,
+        bucketName: policy.bucket,
+        fileId: result.fileId
+      };
+    } catch (error) {
+      callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
+      throw error;
+    }
+  }
+  
+  // 上传单个片段
+  private static uploadPart(
+    uploadUrl: string,
+    partBlob: Blob,
+    callbacks?: MinioProgressCallbacks,
+    progressDetails?: UploadProgressDetails
+  ): Promise<string> {
+    return new Promise((resolve, reject) => {
+      const xhr = new XMLHttpRequest();
+      
+      xhr.upload.onprogress = (event) => {
+        if (event.lengthComputable && callbacks?.onProgress) {
+          const partProgress = Math.round((event.loaded / event.total) * 100);
+          callbacks.onProgress({
+            stage: 'uploading',
+            message: `上传文件片段 ${progressDetails?.partNumber}/${progressDetails?.totalParts} (${partProgress}%)`,
+            progress: Math.round((
+              (progressDetails?.partNumber ? (progressDetails.partNumber - 1) * (progressDetails.partSize || 0) : 0) + event.loaded
+            ) / (progressDetails?.totalSize || 1) * 100),
+            details: {
+              ...progressDetails,
+              loaded: event.loaded,
+              total: event.total
+            },
+            timestamp: Date.now()
+          });
+        }
+      };
+      
+      xhr.onload = () => {
+        if (xhr.status >= 200 && xhr.status < 300) {
+          const etag = xhr.getResponseHeader('ETag')?.replace(/"/g, '') || '';
+          resolve(etag);
+        } else {
+          reject(new Error(`上传片段失败: ${xhr.status} ${xhr.statusText}`));
+        }
+      };
+      
+      xhr.onerror = () => reject(new Error('上传片段失败'));
+      
+      xhr.open('PUT', uploadUrl);
+      xhr.send(partBlob);
+      
+      if (callbacks?.signal) {
+        if ('addEventListener' in callbacks.signal) {
+          callbacks.signal.addEventListener('abort', () => {
+            xhr.abort();
+            reject(new Error('上传已取消'));
+          });
+        }
+      }
+    });
+  }
+  
+  // 完成分段上传
+  private static async completeMultipartUpload(
+    policy: MinioMultipartUploadPolicy,
+    key: string,
+    uploadedParts: UploadPart[]
+  ): Promise<{ fileId: number }> {
+    const response = await fileClient["multipart-complete"].$post({
+      json: {
+        bucket: policy.bucket,
+        key,
+        uploadId: policy.uploadId,
+        parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
+      }
+    });
+    
+    if (!response.ok) {
+      throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
+    }
+    
+    return response.json();
+  }
+}
+
+export class MinIOXHRUploader {
+  /**
+   * 使用XHR上传文件到MinIO(H5环境)
+   */
+  static upload(
+    policy: MinioUploadPolicy,
+    file: File | Blob,
+    key: string,
+    callbacks?: MinioProgressCallbacks
+  ): Promise<UploadResult> {
+    const formData = new FormData();
+
+    // 添加 MinIO 需要的字段
+    Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
+      if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
+        formData.append(k, value);
+      }
+    });
+    formData.append('key', key);
+    formData.append('file', file);
+
+    return new Promise((resolve, reject) => {
+      const xhr = new XMLHttpRequest();
+
+      // 上传进度处理
+      if (callbacks?.onProgress) {
+        xhr.upload.onprogress = (event) => {
+          if (event.lengthComputable) {
+            callbacks.onProgress?.({
+              stage: 'uploading',
+              message: '正在上传文件...',
+              progress: Math.round((event.loaded * 100) / event.total),
+              details: {
+                loaded: event.loaded,
+                total: event.total
+              },
+              timestamp: Date.now()
+            });
+          }
+        };
+      }
+
+      // 完成处理
+      xhr.onload = () => {
+        if (xhr.status >= 200 && xhr.status < 300) {
+          if (callbacks?.onProgress) {
+            callbacks.onProgress({
+              stage: 'complete',
+              message: '文件上传完成',
+              progress: 100,
+              timestamp: Date.now()
+            });
+          }
+          callbacks?.onComplete?.();
+          resolve({
+            fileUrl: `${policy.uploadPolicy.host}/${key}`,
+            fileKey: key,
+            bucketName: policy.uploadPolicy.bucket,
+            fileId: policy.file.id
+          });
+        } else {
+          const error = new Error(`上传失败: ${xhr.status} ${xhr.statusText}`);
+          callbacks?.onError?.(error);
+          reject(error);
+        }
+      };
+
+      // 错误处理
+      xhr.onerror = () => {
+        const error = new Error('上传失败');
+        if (callbacks?.onProgress) {
+          callbacks.onProgress({
+            stage: 'error',
+            message: '文件上传失败',
+            progress: 0,
+            timestamp: Date.now()
+          });
+        }
+        callbacks?.onError?.(error);
+        reject(error);
+      };
+
+      // 根据当前页面协议和 host 配置决定最终的上传地址
+      const currentProtocol = typeof window !== 'undefined' ? window.location.protocol : 'https:';
+      const host = policy.uploadPolicy.host?.startsWith('http')
+        ? policy.uploadPolicy.host
+        : `${currentProtocol}//${policy.uploadPolicy.host}`;
+      
+      xhr.open('POST', host);
+      xhr.send(formData);
+
+      // 处理取消
+      if (callbacks?.signal) {
+        if ('addEventListener' in callbacks.signal) {
+          callbacks.signal.addEventListener('abort', () => {
+            xhr.abort();
+            reject(new Error('上传已取消'));
+          });
+        }
+      }
+    });
+  }
+}
+
+// ==================== 小程序实现 ====================
+export class TaroMinIOMultipartUploader {
+  /**
+   * 使用 Taro 分段上传文件到 MinIO(小程序环境)
+   */
+  static async upload(
+    policy: MinioMultipartUploadPolicy,
+    filePath: string,
+    key: string,
+    callbacks?: MinioProgressCallbacks
+  ): Promise<UploadResult> {
+    const partSize = PART_SIZE;
+    
+    // 获取文件信息
+    const fileInfo = await getFileInfoPromise(filePath);
+    const totalSize = fileInfo.size;
+    const totalParts = Math.ceil(totalSize / partSize);
+    const uploadedParts: UploadPart[] = [];
+    
+    callbacks?.onProgress?.({
+      stage: 'uploading',
+      message: '准备上传文件...',
+      progress: 0,
+      details: {
+        loaded: 0,
+        total: totalSize
+      },
+      timestamp: Date.now()
+    });
+    
+    // 分段上传
+    for (let i = 0; i < totalParts; i++) {
+      if (callbacks?.signal && 'aborted' in callbacks.signal && callbacks.signal.aborted) {
+        throw new Error('上传已取消');
+      }
+      
+      const start = i * partSize;
+      const end = Math.min(start + partSize, totalSize);
+      const partNumber = i + 1;
+      
+      try {
+        // 读取文件片段
+        const partData = await this.readFileSlice(filePath, start, end);
+        
+        const etag = await this.uploadPart(
+          policy.partUrls[i],
+          partData,
+          callbacks,
+          {
+            partNumber,
+            totalParts,
+            partSize: end - start,
+            totalSize
+          }
+        );
+        
+        uploadedParts.push({
+          ETag: etag,
+          PartNumber: partNumber
+        });
+        
+        // 更新进度
+        const progress = Math.round((end / totalSize) * 100);
+        callbacks?.onProgress?.({
+          stage: 'uploading',
+          message: `上传文件片段 ${partNumber}/${totalParts}`,
+          progress,
+          details: {
+            loaded: end,
+            total: totalSize,
+          },
+          timestamp: Date.now()
+        });
+      } catch (error) {
+        callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
+        throw error;
+      }
+    }
+    
+    // 完成上传
+    try {
+      const result = await this.completeMultipartUpload(policy, key, uploadedParts);
+      
+      callbacks?.onProgress?.({
+        stage: 'complete',
+        message: '文件上传完成',
+        progress: 100,
+        timestamp: Date.now()
+      });
+      
+      callbacks?.onComplete?.();
+      return {
+        fileUrl: `${policy.host}/${key}`,
+        fileKey: key,
+        bucketName: policy.bucket,
+        fileId: result.fileId
+      };
+    } catch (error) {
+      callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
+      throw error;
+    }
+  }
+  
+  // 读取文件片段
+  private static async readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
+    return new Promise((resolve, reject) => {
+      try {
+        const fs = Taro?.getFileSystemManager?.();
+        if (!fs) {
+          reject(new Error('小程序文件系统不可用'));
+          return;
+        }
+        
+        const fileData = fs.readFileSync(filePath, undefined, start, end - start);
+        
+        // 确保返回 ArrayBuffer 类型
+        if (typeof fileData === 'string') {
+          // 将字符串转换为 ArrayBuffer
+          const encoder = new TextEncoder();
+          resolve(encoder.encode(fileData).buffer);
+        } else if (fileData instanceof ArrayBuffer) {
+          resolve(fileData);
+        } else {
+          // 处理其他可能的数据类型
+          reject(new Error('文件数据类型不支持'));
+        }
+      } catch (error) {
+        reject(error);
+      }
+    });
+  }
+  
+  // 上传单个片段
+  private static async uploadPart(
+    uploadUrl: string,
+    partData: ArrayBuffer,
+    _callbacks?: MinioProgressCallbacks,
+    _progressDetails?: UploadProgressDetails
+  ): Promise<string> {
+    return new Promise((resolve, reject) => {
+      Taro?.request?.({
+        url: uploadUrl,
+        method: 'PUT',
+        data: partData,
+        header: {
+          'Content-Type': 'application/octet-stream'
+        },
+        success: (res: any) => {
+          if (res.statusCode >= 200 && res.statusCode < 300) {
+            const etag = res.header?.['ETag']?.replace(/"/g, '') || '';
+            resolve(etag);
+          } else {
+            reject(new Error(`上传片段失败: ${res.statusCode}`));
+          }
+        },
+        fail: (error: any) => {
+          reject(new Error(`上传片段失败: ${error.errMsg}`));
+        }
+      }) || reject(new Error('小程序环境不可用'));
+    });
+  }
+  
+  // 完成分段上传
+  private static async completeMultipartUpload(
+    policy: MinioMultipartUploadPolicy,
+    key: string,
+    uploadedParts: UploadPart[]
+  ): Promise<{ fileId: number }> {
+    const response = await fileClient["multipart-complete"].$post({
+      json: {
+        bucket: policy.bucket,
+        key,
+        uploadId: policy.uploadId,
+        parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
+      }
+    });
+    
+    if (!response.ok) {
+      throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
+    }
+    
+    return await response.json();
+  }
+}
+
+export class TaroMinIOUploader {
+  /**
+   * 使用 Taro 上传文件到 MinIO(小程序环境)
+   */
+  static async upload(
+    policy: MinioUploadPolicy,
+    filePath: string,
+    key: string,
+    callbacks?: MinioProgressCallbacks
+  ): Promise<UploadResult> {
+    // 获取文件信息
+    const fileInfo = await getFileInfoPromise(filePath);
+    const totalSize = fileInfo.size;
+    
+    callbacks?.onProgress?.({
+      stage: 'uploading',
+      message: '准备上传文件...',
+      progress: 0,
+      details: {
+        loaded: 0,
+        total: totalSize
+      },
+      timestamp: Date.now()
+    });
+    
+    return new Promise((resolve, reject) => {
+      // 准备表单数据 - 使用对象形式,Taro.uploadFile会自动处理
+      const formData: Record<string, string> = {};
+      
+      // 添加 MinIO 需要的字段
+      Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
+        if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
+          formData[k] = value;
+        }
+      });
+      
+      formData['key'] = key;
+      
+      // 使用 Taro.uploadFile 替代 FormData
+      const uploadTask = Taro.uploadFile({
+        url: policy.uploadPolicy.host,
+        filePath: filePath,
+        name: 'file',
+        formData: formData,
+        header: {
+          'Content-Type': 'multipart/form-data'
+        },
+        success: (res) => {
+          if (res.statusCode >= 200 && res.statusCode < 300) {
+            callbacks?.onProgress?.({
+              stage: 'complete',
+              message: '文件上传完成',
+              progress: 100,
+              timestamp: Date.now()
+            });
+            callbacks?.onComplete?.();
+            resolve({
+              fileUrl: `${policy.uploadPolicy.host}/${key}`,
+              fileKey: key,
+              bucketName: policy.uploadPolicy.bucket,
+              fileId: policy.file.id
+            });
+          } else {
+            reject(new Error(`上传失败: ${res.statusCode}`));
+          }
+        },
+        fail: (error) => {
+          reject(new Error(`上传失败: ${error.errMsg}`));
+        }
+      });
+
+      // 监听上传进度
+      uploadTask.progress((res) => {
+        if (res.totalBytesExpectedToSend > 0) {
+          const currentProgress = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100);
+          callbacks?.onProgress?.({
+            stage: 'uploading',
+            message: `上传中 ${currentProgress}%`,
+            progress: currentProgress,
+            details: {
+              loaded: res.totalBytesSent,
+              total: res.totalBytesExpectedToSend
+            },
+            timestamp: Date.now()
+          });
+        }
+      });
+
+      // 支持取消上传
+      if (callbacks?.signal && 'aborted' in callbacks.signal) {
+        if (callbacks.signal.aborted) {
+          uploadTask.abort();
+          reject(new Error('上传已取消'));
+        }
+        
+        // 监听取消信号
+        const checkAbort = () => {
+          if (callbacks.signal?.aborted) {
+            uploadTask.abort();
+            reject(new Error('上传已取消'));
+          }
+        };
+        
+        // 定期检查取消状态
+        const abortInterval = setInterval(checkAbort, 100);
+        
+        // 清理定时器
+        const cleanup = () => clearInterval(abortInterval);
+        uploadTask.onProgressUpdate = cleanup;
+        uploadTask.onHeadersReceived = cleanup;
+      }
+    });
+  }
+}
+
+// ==================== 统一 API ====================
+/**
+ * 根据运行环境自动选择合适的上传器
+ */
+export class UniversalMinIOMultipartUploader {
+  static async upload(
+    policy: MinioMultipartUploadPolicy,
+    file: File | Blob | string,
+    key: string,
+    callbacks?: MinioProgressCallbacks
+  ): Promise<UploadResult> {
+    if (isBrowser && (file instanceof File || file instanceof Blob)) {
+      return MinIOXHRMultipartUploader.upload(policy, file, key, callbacks);
+    } else if (isMiniProgram && typeof file === 'string') {
+      return TaroMinIOMultipartUploader.upload(policy, file, key, callbacks);
+    } else {
+      throw new Error('不支持的运行环境或文件类型');
+    }
+  }
+}
+
+export class UniversalMinIOUploader {
+  static async upload(
+    policy: MinioUploadPolicy,
+    file: File | Blob | string,
+    key: string,
+    callbacks?: MinioProgressCallbacks
+  ): Promise<UploadResult> {
+    if (isBrowser && (file instanceof File || file instanceof Blob)) {
+      return MinIOXHRUploader.upload(policy, file, key, callbacks);
+    } else if (isMiniProgram && typeof file === 'string') {
+      return TaroMinIOUploader.upload(policy, file, key, callbacks);
+    } else {
+      throw new Error('不支持的运行环境或文件类型');
+    }
+  }
+}
+
+// ==================== 通用函数 ====================
+export async function getUploadPolicy(key: string, fileName: string, fileType?: string, fileSize?: number): Promise<MinioUploadPolicy> {
+  const policyResponse = await fileClient["upload-policy"].$post({
+    json: {
+      path: key,
+      name: fileName,
+      type: fileType,
+      size: fileSize
+    }
+  });
+  if (!policyResponse.ok) {
+    throw new Error('获取上传策略失败');
+  }
+  return policyResponse.json();
+}
+
+export async function getMultipartUploadPolicy(totalSize: number, fileKey: string, fileType?: string, fileName: string = 'unnamed-file') {
+  const policyResponse = await fileClient["multipart-policy"].$post({
+    json: {
+      totalSize,
+      partSize: PART_SIZE,
+      fileKey,
+      type: fileType,
+      name: fileName
+    }
+  });
+  if (!policyResponse.ok) {
+    throw new Error('获取分段上传策略失败');
+  }
+  return await policyResponse.json();
+}
+
+/**
+ * 统一的上传函数,自动适应运行环境
+ */
+export async function uploadMinIOWithPolicy(
+  uploadPath: string,
+  file: File | Blob | string,
+  fileKey: string,
+  callbacks?: MinioProgressCallbacks
+): Promise<UploadResult> {
+  if(uploadPath === '/') uploadPath = '';
+  else{
+    if(!uploadPath.endsWith('/')) uploadPath = `${uploadPath}/`
+    if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, '');
+  }
+  
+  let fileSize: number;
+  let fileType: string | undefined;
+  let fileName: string;
+  
+  if (isBrowser && (file instanceof File || file instanceof Blob)) {
+    fileSize = file.size;
+    fileType = (file as File).type || undefined;
+    fileName = (file as File).name || fileKey;
+  } else if (isMiniProgram && typeof file === 'string') {
+    try {
+      const fileInfo = await getFileInfoPromise(file);
+      fileSize = fileInfo.size;
+      fileType = undefined;
+      fileName = fileKey;
+    } catch {
+      fileSize = 0;
+      fileType = undefined;
+      fileName = fileKey;
+    }
+  } else {
+    throw new Error('不支持的文件类型');
+  }
+  
+  if (fileSize > PART_SIZE) {
+    if (isBrowser && !(file instanceof File)) {
+      throw new Error('不支持的文件类型,无法获取文件名');
+    }
+    
+    const policy = await getMultipartUploadPolicy(
+      fileSize,
+      `${uploadPath}${fileKey}`,
+      fileType,
+      fileName
+    );
+    
+    if (isBrowser) {
+      return MinIOXHRMultipartUploader.upload(policy, file as File | Blob, policy.key, callbacks);
+    } else {
+      return TaroMinIOMultipartUploader.upload(policy, file as string, policy.key, callbacks);
+    }
+  } else {
+    if (isBrowser && !(file instanceof File)) {
+      throw new Error('不支持的文件类型,无法获取文件名');
+    }
+    
+    const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, fileName, fileType, fileSize);
+    
+    if (isBrowser) {
+      return MinIOXHRUploader.upload(policy, file as File | Blob, policy.uploadPolicy.key, callbacks);
+    } else {
+      return TaroMinIOUploader.upload(policy, file as string, policy.uploadPolicy.key, callbacks);
+    }
+  }
+}
+
+// ==================== 小程序工具函数 ====================
+/**
+ * Promise封装的getFileInfo函数
+ */
+async function getFileInfoPromise(filePath: string): Promise<{ size: number }> {
+  return new Promise((resolve, reject) => {
+    const fs = Taro?.getFileSystemManager?.();
+    if (!fs) {
+      reject(new Error('小程序文件系统不可用'));
+      return;
+    }
+    
+    fs.getFileInfo({
+      filePath,
+      success: (res) => {
+        resolve({ size: res.size });
+      },
+      fail: (error) => {
+        reject(new Error(`获取文件信息失败: ${error.errMsg}`));
+      }
+    });
+  });
+}
+
+// 新增:自动适应运行环境的文件选择并上传函数
+/**
+ * 自动适应运行环境:选择文件并上传到 MinIO
+ * 小程序:使用 Taro.chooseImage
+ * H5:使用 input[type="file"]
+ */
+export async function uploadFromSelect(
+  uploadPath: string = '',
+  options: {
+    sourceType?: ('album' | 'camera')[],
+    count?: number,
+    accept?: string,
+    maxSize?: number,
+  } = {},
+  callbacks?: MinioProgressCallbacks
+): Promise<UploadResult> {
+  const { sourceType = ['album', 'camera'], count = 1, accept = '*', maxSize = 10 * 1024 * 1024 } = options;
+
+  if (isMiniProgram) {
+    return new Promise((resolve, reject) => {
+      
+      Taro.chooseImage({
+        count,
+        sourceType: sourceType as any, // 确保类型兼容
+        success: async (res) => {
+          const tempFilePath = res.tempFilePaths[0];
+          const fileName = res.tempFiles[0]?.originalFileObj?.name || tempFilePath.split('/').pop() || 'unnamed-file';
+          
+          try {
+            const result = await uploadMinIOWithPolicy(uploadPath, tempFilePath, fileName, callbacks);
+            resolve(result);
+          } catch (error) {
+            reject(error);
+          }
+        },
+        fail: reject
+      });
+    });
+  } else if (isBrowser) {
+    return new Promise((resolve, reject) => {
+      const input = document.createElement('input');
+      input.type = 'file';
+      input.accept = accept;
+      input.multiple = count > 1;
+      
+      input.onchange = async (event) => {
+        const files = (event.target as HTMLInputElement).files;
+        if (!files || files.length === 0) {
+          reject(new Error('未选择文件'));
+          return;
+        }
+        
+        const file = files[0];
+        if (file.size > maxSize) {
+          reject(new Error(`文件大小超过限制: ${maxSize / 1024 / 1024}MB`));
+          return;
+        }
+        
+        const fileName = file.name || 'unnamed-file';
+        
+        try {
+          const result = await uploadMinIOWithPolicy(uploadPath, file, fileName, callbacks);
+          resolve(result);
+        } catch (error) {
+          reject(error);
+        }
+      };
+      
+      input.click();
+    });
+  } else {
+    throw new Error('不支持的运行环境');
+  }
+}
+
+// 默认导出
+export default {
+  MinIOXHRMultipartUploader,
+  MinIOXHRUploader,
+  TaroMinIOMultipartUploader,
+  TaroMinIOUploader,
+  UniversalMinIOMultipartUploader,
+  UniversalMinIOUploader,
+  getUploadPolicy,
+  getMultipartUploadPolicy,
+  uploadMinIOWithPolicy,
+  uploadFromSelect
+};

+ 2 - 0
mini-talent/src/utils/platform.ts

@@ -0,0 +1,2 @@
+// 从共享UI组件包重新导出平台工具函数
+export { getPlatform, isWeapp, isH5 } from '@d8d/mini-shared-ui-components'

+ 88 - 0
mini-talent/src/utils/response-polyfill.ts

@@ -0,0 +1,88 @@
+class ResponsePolyfill {
+    constructor(
+      public body: string | ArrayBuffer | null,
+      public init: {
+        status?: number
+        statusText?: string
+        headers?: Record<string, string>
+      } = {}
+    ) {}
+  
+    get ok(): boolean {
+      return this.status >= 200 && this.status < 300
+    }
+  
+    get status(): number {
+      return this.init.status || 200
+    }
+  
+    get statusText(): string {
+      return this.init.statusText || 'OK'
+    }
+  
+    get headers(): Headers {
+      return new Headers(this.init.headers || {})
+    }
+  
+    get bodyUsed(): boolean {
+      return false // 小程序环境简单实现
+    }
+  
+    async arrayBuffer(): Promise<ArrayBuffer> {
+      if (this.body instanceof ArrayBuffer) {
+        return this.body
+      }
+      throw new Error('Not implemented')
+    }
+  
+    async text(): Promise<string> {
+      if (typeof this.body === 'string') {
+        return this.body
+      }
+      throw new Error('Not implemented')
+    }
+  
+    async json<T = any>(): Promise<T> {
+      if (typeof this.body === 'string') {
+        try {
+          return JSON.parse(this.body)
+        } catch (e) {
+          throw new Error('Invalid JSON')
+        }
+      }
+      throw new Error('Not implemented')
+    }
+  
+    clone(): ResponsePolyfill {
+      return new ResponsePolyfill(this.body, { ...this.init })
+    }
+  
+    static json(data: any, init?: ResponseInit): ResponsePolyfill {
+      const headers = new Headers(init && 'headers' in init ? init.headers : undefined)
+      if (!headers.has('Content-Type')) {
+        headers.set('Content-Type', 'application/json')
+      }
+      return new ResponsePolyfill(JSON.stringify(data), {
+        ...init,
+        headers: Object.fromEntries(headers.entries())
+      })
+    }
+  
+    static error(): ResponsePolyfill {
+      return new ResponsePolyfill(null, { status: 0, statusText: 'Network Error' })
+    }
+  
+    static redirect(url: string, status: number): ResponsePolyfill {
+      return new ResponsePolyfill(null, {
+        status,
+        headers: { Location: url }
+      })
+    }
+  }
+  
+  // 全局注册(如果需要)
+  if (typeof globalThis.Response === 'undefined') {
+    globalThis.Response = ResponsePolyfill as any
+  }
+  
+  export default ResponsePolyfill

+ 182 - 0
mini-talent/src/utils/rpc-client.ts

@@ -0,0 +1,182 @@
+import Taro from '@tarojs/taro'
+import { hc } from 'hono/client'
+import ResponsePolyfill from './response-polyfill'
+
+// 刷新token的函数
+let isRefreshing = false
+let refreshSubscribers: ((token: string) => void)[] = []
+
+// 执行token刷新
+const refreshToken = async (): Promise<string | null> => {
+  if (isRefreshing) {
+    // 如果已经在刷新,等待结果
+    return new Promise((resolve) => {
+      refreshSubscribers.push((token) => {
+        resolve(token)
+      })
+    })
+  }
+
+  isRefreshing = true
+  try {
+    const refreshToken = Taro.getStorageSync('enterprise_refresh_token')
+    if (!refreshToken) {
+      throw new Error('未找到刷新token')
+    }
+
+    // 调用刷新token接口
+    const response = await Taro.request({
+      url: `${process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'}/api/v1/yongren/auth/refresh-token`,
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${refreshToken}`
+      }
+    })
+
+    if (response.statusCode === 200) {
+      const { token, refresh_token: newRefreshToken } = response.data
+      Taro.setStorageSync('enterprise_token', token)
+      if (newRefreshToken) {
+        Taro.setStorageSync('enterprise_refresh_token', newRefreshToken)
+      }
+
+      // 通知所有等待的请求
+      refreshSubscribers.forEach(callback => callback(token))
+      refreshSubscribers = []
+      return token
+    } else {
+      throw new Error('刷新token失败')
+    }
+  } catch (error) {
+    console.error('刷新token失败:', error)
+    // 清除token,跳转到登录页
+    Taro.removeStorageSync('enterprise_token')
+    Taro.removeStorageSync('enterprise_refresh_token')
+    Taro.removeStorageSync('enterpriseUserInfo')
+
+    // 跳转到登录页
+    Taro.showToast({
+      title: '登录已过期,请重新登录',
+      icon: 'none'
+    })
+    setTimeout(() => {
+      Taro.redirectTo({
+        url: '/pages/login/index'
+      })
+    }, 1500)
+
+    return null
+  } finally {
+    isRefreshing = false
+  }
+}
+
+// API配置
+const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'
+
+// 完整的API地址
+// const BASE_URL = `${API_BASE_URL}/api/${API_VERSION}`
+
+// 创建自定义fetch函数,适配Taro.request,支持token自动刷新
+const taroFetch: any = async (input, init) => {
+  const url = typeof input === 'string' ? input : input.url
+  const method = init.method || 'GET'
+
+  const requestHeaders: Record<string, string> = init.headers;
+
+  const keyOfContentType = Object.keys(requestHeaders).find(item => item.toLowerCase() === 'content-type')
+  if (!keyOfContentType) {
+    requestHeaders['content-type'] = 'application/json'
+  }
+
+  // 构建Taro请求选项
+  const options: Taro.request.Option = {
+    url,
+    method: method as any,
+    data: init.body,
+    header: requestHeaders
+  }
+
+  // 添加token - 优先使用企业token,兼容mini_token
+  let token = Taro.getStorageSync('enterprise_token')
+  if (!token) {
+    token = Taro.getStorageSync('mini_token')
+  }
+  if (token) {
+    options.header = {
+      ...options.header,
+      'Authorization': `Bearer ${token}`
+    }
+  }
+
+  // 发送请求
+  const sendRequest = async (): Promise<any> => {
+    try {
+      console.log('API请求:', options.url)
+      const response = await Taro.request(options)
+
+      const responseHeaders = response.header;
+
+      // 处理204 No Content响应,不设置body
+      const body = response.statusCode === 204
+        ? null
+        : responseHeaders['content-type']!.includes('application/json')
+          ? JSON.stringify(response.data)
+          : response.data;
+
+      return new ResponsePolyfill(
+        body,
+        {
+          status: response.statusCode,
+          statusText: response.errMsg || 'OK',
+          headers: responseHeaders
+        }
+      )
+    } catch (error) {
+      console.error('API Error:', error)
+      throw error
+    }
+  }
+
+  try {
+    let response = await sendRequest()
+
+    // 检查是否为401错误,尝试刷新token
+    if (response.status === 401 && token) {
+      console.log('检测到401错误,尝试刷新token...')
+      const newToken = await refreshToken()
+
+      if (newToken) {
+        // 更新请求header中的token
+        options.header = {
+          ...options.header,
+          'Authorization': `Bearer ${newToken}`
+        }
+
+        // 重试原始请求
+        response = await sendRequest()
+      } else {
+        // 刷新失败,返回原始401响应
+        return response
+      }
+    }
+
+    return response
+  } catch (error) {
+    console.error('API请求失败:', error)
+    Taro.showToast({
+      title: error.message || '网络错误',
+      icon: 'none'
+    })
+    throw error
+  }
+}
+
+// 创建Hono RPC客户端
+export const rpcClient = <T extends any>() => {
+  // @ts-ignore
+  return hc<T>(`${API_BASE_URL}`, {
+    fetch: taroFetch
+  })
+}

+ 4 - 0
mini-talent/stylelint.config.mjs

@@ -0,0 +1,4 @@
+/** @type {import('stylelint').Config} */
+export default {
+  extends: "stylelint-config-standard",
+};

+ 28 - 0
mini-talent/tailwind.config.js

@@ -0,0 +1,28 @@
+const { iconsPlugin, getIconCollections } = require("@egoist/tailwindcss-icons")
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: [
+  './src/**/*.{html,js,ts,jsx,tsx}',
+  '../mini-ui-packages/mini-enterprise-auth-ui/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/mini-shared-ui-components/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/yongren-dashboard-ui/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/yongren-order-management-ui/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/yongren-settings-ui/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/yongren-shared-ui/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/yongren-statistics-ui/src/**/*.{ts,tsx}',
+  '../mini-ui-packages/yongren-talent-management-ui/src/**/*.{ts,tsx}',
+  ],
+  theme: {
+  extend: {},
+  },
+  plugins: [
+    iconsPlugin({
+      // Select the icon collections you want to use
+      collections: getIconCollections(["mdi", "lucide", "heroicons", "heroicons-outline", "heroicons-solid"]),
+    }),
+  ],
+  corePlugins: {
+  preflight: false,
+  },
+}

+ 1 - 0
mini-talent/tests/__mocks__/fileMock.js

@@ -0,0 +1 @@
+module.exports = 'test-file-stub'

+ 1 - 0
mini-talent/tests/__mocks__/styleMock.js

@@ -0,0 +1 @@
+module.exports = {}

+ 100 - 0
mini-talent/tests/__mocks__/taroMock.ts

@@ -0,0 +1,100 @@
+/**
+ * Taro API Mock 文件
+ * 通过 jest.config.js 的 moduleNameMapper 重定向 @tarojs/taro 到这里
+ */
+
+// 创建所有 Taro API 的 mock 函数
+export const mockShowToast = jest.fn()
+export const mockShowLoading = jest.fn()
+export const mockHideLoading = jest.fn()
+export const mockNavigateTo = jest.fn()
+export const mockNavigateBack = jest.fn()
+export const mockSwitchTab = jest.fn()
+export const mockShowModal = jest.fn()
+export const mockReLaunch = jest.fn()
+export const mockOpenCustomerServiceChat = jest.fn()
+export const mockUseRouter = jest.fn()
+export const mockRequestPayment = jest.fn()
+export const mockGetEnv = jest.fn()
+export const mockUseLoad = jest.fn()
+export const mockUseShareAppMessage = jest.fn()
+export const mockUseShareTimeline = jest.fn()
+export const mockGetCurrentInstance = jest.fn()
+
+// 环境类型常量
+export const ENV_TYPE = {
+  WEAPP: 'WEAPP',
+  WEB: 'WEB',
+  RN: 'RN',
+  SWAN: 'SWAN',
+  ALIPAY: 'ALIPAY',
+  TT: 'TT',
+  QQ: 'QQ',
+  JD: 'JD',
+  HARMONY: 'HARMONY'
+}
+
+// 导出所有 mock 函数,便于在测试中访问
+export default {
+  // UI 相关
+  showToast: mockShowToast,
+  showLoading: mockShowLoading,
+  hideLoading: mockHideLoading,
+  showModal: mockShowModal,
+
+  // 导航相关
+  navigateTo: mockNavigateTo,
+  navigateBack: mockNavigateBack,
+  switchTab: mockSwitchTab,
+  reLaunch: mockReLaunch,
+  useRouter: () => mockUseRouter(),
+  useLoad: (callback: any) => mockUseLoad(callback),
+
+  // 微信相关
+  openCustomerServiceChat: mockOpenCustomerServiceChat,
+  requestPayment: mockRequestPayment,
+
+  // 系统信息
+  getSystemInfoSync: () => ({
+    statusBarHeight: 20
+  }),
+  getMenuButtonBoundingClientRect: () => ({
+    width: 87,
+    height: 32,
+    top: 48,
+    right: 314,
+    bottom: 80,
+    left: 227
+  }),
+  getEnv: mockGetEnv,
+
+  // 分享相关
+  useShareAppMessage: mockUseShareAppMessage,
+  useShareTimeline: mockUseShareTimeline,
+
+  // 实例相关
+  getCurrentInstance: mockGetCurrentInstance,
+
+  // 环境类型常量
+  ENV_TYPE
+}
+
+// 为命名导入导出所有函数
+export {
+  mockShowToast as showToast,
+  mockShowLoading as showLoading,
+  mockHideLoading as hideLoading,
+  mockShowModal as showModal,
+  mockNavigateTo as navigateTo,
+  mockNavigateBack as navigateBack,
+  mockSwitchTab as switchTab,
+  mockReLaunch as reLaunch,
+  mockUseRouter as useRouter,
+  mockUseLoad as useLoad,
+  mockOpenCustomerServiceChat as openCustomerServiceChat,
+  mockRequestPayment as requestPayment,
+  mockGetEnv as getEnv,
+  mockUseShareAppMessage as useShareAppMessage,
+  mockUseShareTimeline as useShareTimeline,
+  mockGetCurrentInstance as getCurrentInstance
+}

+ 13 - 0
mini-talent/tests/__snapshots__/example.test.tsx.snap

@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`Taro 组件测试示例 应该匹配快照 1`] = `
+<div
+  class="test-component"
+>
+  <span
+    class="btn"
+  >
+    点击我
+  </span>
+</div>
+`;

+ 37 - 0
mini-talent/tests/components/Button.test.tsx

@@ -0,0 +1,37 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { Button } from '@tarojs/components'
+
+describe('Button 组件测试', () => {
+  test('应该正确渲染按钮', () => {
+    render(<Button>测试按钮</Button>)
+
+    const button = screen.getByRole('button')
+    expect(button).toBeInTheDocument()
+    expect(button).toHaveTextContent('测试按钮')
+  })
+
+  test('应该响应点击事件', () => {
+    const handleClick = jest.fn()
+
+    render(<Button onClick={handleClick}>可点击按钮</Button>)
+
+    const button = screen.getByRole('button')
+    fireEvent.click(button)
+
+    expect(handleClick).toHaveBeenCalledTimes(1)
+  })
+
+  test('应该禁用按钮', () => {
+    render(<Button disabled>禁用按钮</Button>)
+
+    const button = screen.getByRole('button')
+    expect(button).toBeDisabled()
+  })
+
+  test('应该应用自定义类名', () => {
+    render(<Button className="custom-class">自定义按钮</Button>)
+
+    const button = screen.getByRole('button')
+    expect(button).toHaveClass('custom-class')
+  })
+})

+ 43 - 0
mini-talent/tests/example.test.tsx

@@ -0,0 +1,43 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { Text, View } from '@tarojs/components'
+
+// 简单的测试组件
+const TestComponent = () => {
+  return (
+    <View className="test-component">
+      <Text className="btn">点击我</Text>
+    </View>
+  )
+}
+
+describe('Taro 组件测试示例', () => {
+  test('应该正确渲染组件', () => {
+    render(<TestComponent />)
+
+    const button = screen.getByText('点击我')
+    expect(button).toBeInTheDocument()
+    expect(button).toHaveClass('btn')
+  })
+
+  test('应该响应点击事件', () => {
+    const handleClick = jest.fn()
+
+    const InteractiveComponent = () => (
+      <View className="test-component">
+        <Text className="btn" onClick={handleClick}>点击我</Text>
+      </View>
+    )
+
+    render(<InteractiveComponent />)
+
+    const button = screen.getByText('点击我')
+    fireEvent.click(button)
+
+    expect(handleClick).toHaveBeenCalledTimes(1)
+  })
+
+  test('应该匹配快照', () => {
+    const { container } = render(<TestComponent />)
+    expect(container.firstChild).toMatchSnapshot()
+  })
+})

+ 440 - 0
mini-talent/tests/setup.ts

@@ -0,0 +1,440 @@
+import '@testing-library/jest-dom'
+
+// 扩展全局类型以支持 Taro 配置测试
+declare var defineAppConfig: (config: any) => any
+
+/* eslint-disable react/display-name */
+
+// 设置环境变量
+process.env.TARO_ENV = 'h5'
+process.env.TARO_PLATFORM = 'web'
+process.env.SUPPORT_TARO_POLYFILL = 'disabled'
+
+// 定义 defineAppConfig 全局函数用于测试 Taro 配置文件
+;(global as any).defineAppConfig = (config: any) => config
+
+// Mock Taro 组件
+// eslint-disable-next-line react/display-name
+jest.mock('@tarojs/components', () => {
+  const React = require('react')
+  const MockView = React.forwardRef((props: any, ref: any) => {
+    const { children, ...restProps } = props
+    return React.createElement('div', { ...restProps, ref }, children)
+  })
+  MockView.displayName = 'MockView'
+
+  const MockScrollView = React.forwardRef((props: any, ref: any) => {
+    const {
+      children,
+      onScroll,
+      onTouchStart,
+      onScrollEnd,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollY,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      showScrollbar,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollTop,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollWithAnimation,
+      ...restProps
+    } = props
+    return React.createElement('div', {
+      ...restProps,
+      ref,
+      onScroll: (e: any) => {
+        if (onScroll) onScroll(e)
+      },
+      onTouchStart: (e: any) => {
+        if (onTouchStart) onTouchStart(e)
+      },
+      onTouchEnd: () => {
+        if (onScrollEnd) onScrollEnd()
+      },
+      style: {
+        overflow: 'auto',
+        height: '200px',
+        ...restProps.style
+      }
+    }, children)
+  })
+  MockScrollView.displayName = 'MockScrollView'
+
+  return {
+    View: MockView,
+    ScrollView: MockScrollView,
+    Text: (() => {
+      const MockText = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('span', { ...restProps, ref }, children)
+      })
+      MockText.displayName = 'MockText'
+      return MockText
+    })(),
+    Button: (() => {
+      const MockButton = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('button', { ...restProps, ref }, children)
+      })
+      MockButton.displayName = 'MockButton'
+      return MockButton
+    })(),
+    Input: (() => {
+      const MockInput = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { ...restProps, ref })
+      })
+      MockInput.displayName = 'MockInput'
+      return MockInput
+    })(),
+    Textarea: (() => {
+      const MockTextarea = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('textarea', { ...restProps, ref }, children)
+      })
+      MockTextarea.displayName = 'MockTextarea'
+      return MockTextarea
+    })(),
+    Image: (() => {
+      const MockImage = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('img', { ...restProps, ref })
+      })
+      MockImage.displayName = 'MockImage'
+      return MockImage
+    })(),
+    Form: (() => {
+      const MockForm = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('form', { ...restProps, ref }, children)
+      })
+      MockForm.displayName = 'MockForm'
+      return MockForm
+    })(),
+    Label: (() => {
+      const MockLabel = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('label', { ...restProps, ref }, children)
+      })
+      MockLabel.displayName = 'MockLabel'
+      return MockLabel
+    })(),
+    Picker: (() => {
+      const MockPicker = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('div', { ...restProps, ref }, children)
+      })
+      MockPicker.displayName = 'MockPicker'
+      return MockPicker
+    })(),
+    Switch: (() => {
+      const MockSwitch = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { type: 'checkbox', ...restProps, ref })
+      })
+      MockSwitch.displayName = 'MockSwitch'
+      return MockSwitch
+    })(),
+    Slider: (() => {
+      const MockSlider = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { type: 'range', ...restProps, ref })
+      })
+      MockSlider.displayName = 'MockSlider'
+      return MockSlider
+    })(),
+    Radio: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('input', { type: 'radio', ...restProps, ref }, children)
+    }),
+    RadioGroup: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Checkbox: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('input', { type: 'checkbox', ...restProps, ref }, children)
+    }),
+    CheckboxGroup: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Progress: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('progress', { ...restProps, ref })
+    }),
+    RichText: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MovableArea: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MovableView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Swiper: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    SwiperItem: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Navigator: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('a', { ...restProps, ref }, children)
+    }),
+    Audio: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('audio', { ...restProps, ref })
+    }),
+    Video: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('video', { ...restProps, ref }, children)
+    }),
+    Camera: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    LivePlayer: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    LivePusher: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Map: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Canvas: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('canvas', { ...restProps, ref }, children)
+    }),
+    OpenData: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    WebView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('iframe', { ...restProps, ref }, children)
+    }),
+    Ad: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    OfficialAccount: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CoverView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CoverImage: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('img', { ...restProps, ref })
+    }),
+    FunctionalPageNavigator: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    AdContent: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MatchMedia: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    PageContainer: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    ShareElement: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    KeyboardAccessory: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    RootPortal: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    PageMeta: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    NavigationBar: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Block: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Import: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Include: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Template: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Slot: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    NativeSlot: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CustomWrapper: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Editor: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    VoipRoom: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    AdCustom: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    })
+  }
+})
+
+// 模拟 MutationObserver
+// @ts-ignore
+global.MutationObserver = class {
+  disconnect() {}
+  observe(_element: any, _initObject: any) {}
+  takeRecords() { return [] }
+}
+
+// 模拟 IntersectionObserver
+// @ts-ignore
+global.IntersectionObserver = class {
+  constructor(fn: (args: any[]) => void) {
+    setTimeout(() => {
+      fn([{ isIntersecting: true }])
+    }, 1000)
+  }
+
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+  takeRecords() { return [] }
+  root: null = null
+  rootMargin: string = ''
+  thresholds: number[] = []
+}
+
+// 模拟 ResizeObserver
+// @ts-ignore
+global.ResizeObserver = class {
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+}
+
+// 模拟 matchMedia
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: jest.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: jest.fn(), // deprecated
+    removeListener: jest.fn(), // deprecated
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  })),
+})
+
+// 模拟 getComputedStyle
+Object.defineProperty(window, 'getComputedStyle', {
+  value: () => ({
+    getPropertyValue: (prop: string) => {
+      return {
+        'font-size': '16px',
+        'font-family': 'Arial',
+        color: 'rgb(0, 0, 0)',
+        'background-color': 'rgb(255, 255, 255)',
+        width: '100px',
+        height: '100px',
+        top: '0px',
+        left: '0px',
+        right: '0px',
+        bottom: '0px',
+        x: '0px',
+        y: '0px'
+      }[prop] || ''
+    }
+  })
+})
+
+// 模拟 Element.prototype.getBoundingClientRect
+Element.prototype.getBoundingClientRect = jest.fn(() => ({
+  width: 100,
+  height: 100,
+  top: 0,
+  left: 0,
+  bottom: 100,
+  right: 100,
+  x: 0,
+  y: 0,
+  toJSON: () => ({
+    width: 100,
+    height: 100,
+    top: 0,
+    left: 0,
+    bottom: 100,
+    right: 100,
+    x: 0,
+    y: 0
+  })
+}))
+
+// 静默 console.error 在测试中
+const originalConsoleError = console.error
+console.error = (...args: any[]) => {
+  // 检查是否在测试环境中(通过 Jest 环境变量判断)
+  const isTestEnv = process.env.JEST_WORKER_ID !== undefined ||
+                    typeof jest !== 'undefined'
+
+  // 在测试环境中静默错误输出,除非是重要错误
+  if (isTestEnv && !args[0]?.includes?.('重要错误')) {
+    return
+  }
+  originalConsoleError(...args)
+}
+
+// Mock 常用 UI 组件
+jest.mock('@/components/ui/dialog', () => {
+  const React = require('react')
+  return {
+    Dialog: ({ open, children }: any) => open ? React.createElement('div', { 'data-testid': 'dialog' }, children) : null,
+    DialogContent: ({ children, className }: any) => React.createElement('div', { className }, children),
+    DialogHeader: ({ children, className }: any) => React.createElement('div', { className }, children),
+    DialogTitle: ({ children, className }: any) => React.createElement('div', { className }, children),
+    DialogFooter: ({ children, className }: any) => React.createElement('div', { className }, children)
+  }
+})
+
+export {}

+ 59 - 0
mini-talent/tests/yongren-api.test.ts

@@ -0,0 +1,59 @@
+import {
+  channelClient,
+  companyClient,
+  disabilityClient,
+  orderClient,
+  platformClient,
+  salaryClient,
+  enterpriseAuthClient,
+  enterpriseCompanyClient,
+  enterpriseDisabilityClient,
+} from '../src/api'
+
+describe('用人方小程序RPC客户端', () => {
+  test('Allin系统模块客户端应正确定义', () => {
+    expect(channelClient).toBeDefined()
+    expect(companyClient).toBeDefined()
+    expect(disabilityClient).toBeDefined()
+    expect(orderClient).toBeDefined()
+    expect(platformClient).toBeDefined()
+    expect(salaryClient).toBeDefined()
+  })
+
+  test('企业专用客户端应正确定义', () => {
+    expect(enterpriseAuthClient).toBeDefined()
+    expect(enterpriseCompanyClient).toBeDefined()
+    expect(enterpriseDisabilityClient).toBeDefined()
+  })
+
+  test('客户端应包含预期的API方法', () => {
+    // 检查企业认证客户端方法
+    expect(enterpriseAuthClient.login).toBeDefined()
+    expect(enterpriseAuthClient.logout).toBeDefined()
+    expect(enterpriseAuthClient.me).toBeDefined()
+    expect(enterpriseAuthClient['refresh-token']).toBeDefined()
+
+    // 检查企业统计客户端方法
+    expect(enterpriseCompanyClient.overview).toBeDefined()
+    expect(enterpriseCompanyClient[':id']['talents']).toBeDefined()
+    expect(enterpriseCompanyClient['allocations/recent']).toBeDefined()
+
+    // 检查人才扩展客户端方法
+    expect(enterpriseDisabilityClient[':id']['work-history']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id']['salary-history']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id']['credit-info']).toBeDefined()
+    expect(enterpriseDisabilityClient[':id'].videos).toBeDefined()
+  })
+
+  test('企业认证客户端方法应具备正确的HTTP方法', () => {
+    expect(enterpriseAuthClient.login.$post).toBeDefined()
+    expect(enterpriseAuthClient.logout.$post).toBeDefined()
+    expect(enterpriseAuthClient.me.$get).toBeDefined()
+    expect(enterpriseAuthClient['refresh-token'].$post).toBeDefined()
+  })
+
+  test('企业统计客户端方法应具备正确的HTTP方法', () => {
+    expect(enterpriseCompanyClient.overview.$get).toBeDefined()
+    expect(enterpriseCompanyClient['allocations/recent'].$get).toBeDefined()
+  })
+})

+ 46 - 0
mini-talent/tests/yongren-components.test.tsx

@@ -0,0 +1,46 @@
+// @ts-ignore
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import YongrenTabBarLayout from '../src/layouts/yongren-tab-bar-layout'
+import { UserStatusBar } from '../src/components/ui/user-status-bar'
+import { PageContainer } from '../src/components/ui/page-container'
+
+describe('用人方小程序布局组件', () => {
+  test('YongrenTabBarLayout应正确渲染', () => {
+    render(
+      <YongrenTabBarLayout activeKey="dashboard">
+        <div>测试内容</div>
+      </YongrenTabBarLayout>
+    )
+
+    // 检查底部导航标签
+    expect(screen.getByText('首页')).toBeDefined()
+    expect(screen.getByText('人才')).toBeDefined()
+    expect(screen.getByText('订单')).toBeDefined()
+    expect(screen.getByText('数据')).toBeDefined()
+    expect(screen.getByText('设置')).toBeDefined()
+  })
+
+  test('UserStatusBar应正确渲染用户信息', () => {
+    render(
+      <UserStatusBar
+        userName="测试用户"
+        companyName="测试公司"
+        notificationCount={3}
+      />
+    )
+
+    expect(screen.getByText('测试用户')).toBeDefined()
+    expect(screen.getByText('测试公司')).toBeDefined()
+  })
+
+  test('PageContainer应正确渲染子内容', () => {
+    render(
+      <PageContainer>
+        <div>页面内容</div>
+      </PageContainer>
+    )
+
+    expect(screen.getByText('页面内容')).toBeDefined()
+  })
+})

+ 89 - 0
mini-talent/tests/yongren-routes.test.ts

@@ -0,0 +1,89 @@
+// 在导入 app.config.ts 之前定义全局 defineAppConfig 函数
+// @ts-ignore
+global.defineAppConfig = (config: any) => config
+
+// 现在导入实际的配置文件
+import appConfig from '../src/app.config'
+
+describe('用人方小程序路由配置(实际测试配置文件)', () => {
+  test('应包含7个用人方小程序页面', () => {
+    const yongrenPages = appConfig.pages.filter((page: string) => page.includes('yongren'))
+    expect(yongrenPages).toHaveLength(7)
+  })
+
+  test('用人方页面应位于页面列表开头', () => {
+    // 检查前7个页面都是用人方页面
+    const firstSevenPages = appConfig.pages.slice(0, 7)
+    const allAreYongren = firstSevenPages.every(page => page.includes('yongren'))
+    expect(allAreYongren).toBe(true)
+  })
+
+  test('应包含正确的页面路径', () => {
+    const expectedPages = [
+      'pages/yongren/dashboard/index',
+      'pages/yongren/talent/list/index',
+      'pages/yongren/talent/detail/index',
+      'pages/yongren/order/list/index',
+      'pages/yongren/order/detail/index',
+      'pages/yongren/statistics/index',
+      'pages/yongren/settings/index',
+    ]
+
+    expectedPages.forEach(page => {
+      expect(appConfig.pages).toContain(page)
+    })
+  })
+
+  test('现有页面不应被移除', () => {
+    const existingPages = [
+      'pages/profile/index',
+      'pages/login/index',
+    ]
+
+    existingPages.forEach(page => {
+      expect(appConfig.pages).toContain(page)
+    })
+  })
+
+  test('tabBar配置应正确设置', () => {
+    expect(appConfig.tabBar.custom).toBe(true)
+    expect(appConfig.tabBar.color).toBe('#6b7280')
+    expect(appConfig.tabBar.selectedColor).toBe('#3b82f6')
+    expect(appConfig.tabBar.backgroundColor).toBe('#ffffff')
+    expect(appConfig.tabBar.list).toHaveLength(5)
+
+    // 检查tabBar项目
+    const tabBarItems = appConfig.tabBar.list
+    expect(tabBarItems[0].pagePath).toBe('pages/yongren/dashboard/index')
+    expect(tabBarItems[0].text).toBe('首页')
+    expect(tabBarItems[1].pagePath).toBe('pages/yongren/talent/list/index')
+    expect(tabBarItems[1].text).toBe('人才')
+    expect(tabBarItems[2].pagePath).toBe('pages/yongren/order/list/index')
+    expect(tabBarItems[2].text).toBe('订单')
+    expect(tabBarItems[3].pagePath).toBe('pages/yongren/statistics/index')
+    expect(tabBarItems[3].text).toBe('数据')
+    expect(tabBarItems[4].pagePath).toBe('pages/profile/index')
+    expect(tabBarItems[4].text).toBe('设置')
+  })
+
+  test('window配置应正确设置', () => {
+    expect(appConfig.window.navigationBarBackgroundColor).toBe('#3b82f6')
+    expect(appConfig.window.navigationBarTitleText).toBe('用人方小程序')
+    expect(appConfig.window.backgroundTextStyle).toBe('light')
+    expect(appConfig.window.navigationBarTextStyle).toBe('white')
+    expect(appConfig.window.navigationStyle).toBe('custom')
+  })
+
+  test('第一个页面应为仪表板页面', () => {
+    expect(appConfig.pages[0]).toBe('pages/yongren/dashboard/index')
+  })
+
+  test('tabBar页面应正确对应实际页面', () => {
+    const tabBarPaths = appConfig.tabBar.list.map(item => item.pagePath)
+
+    // 所有tabBar路径都应在pages列表中
+    tabBarPaths.forEach(path => {
+      expect(appConfig.pages).toContain(path)
+    })
+  })
+})

+ 34 - 0
mini-talent/tsconfig.json

@@ -0,0 +1,34 @@
+{
+  "compilerOptions": {
+    "target": "es2017",
+    "module": "NodeNext",
+    "removeComments": false,
+    "preserveConstEnums": true,
+    "moduleResolution": "nodenext",
+    "experimentalDecorators": true,
+    "noImplicitAny": false,
+    "allowSyntheticDefaultImports": true,
+    "outDir": "lib",
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "strictNullChecks": true,
+    "sourceMap": true,
+    "rootDir": ".",
+    "jsx": "react-jsx",
+    "allowJs": true,
+    "resolveJsonModule": true,
+    "typeRoots": [
+      "node_modules/@types"
+    ],
+    "paths": {
+      "@/*": ["./src/*"],
+      "~/*": ["./tests/*"]
+    }
+  },
+  "include": ["./src", "./types", "./config", "./tests"],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ],
+  "compileOnSave": false
+}

+ 29 - 0
mini-talent/types/global.d.ts

@@ -0,0 +1,29 @@
+/// <reference types="@tarojs/taro" />
+
+declare module '*.png';
+declare module '*.gif';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.svg';
+declare module '*.css';
+declare module '*.less';
+declare module '*.scss';
+declare module '*.sass';
+declare module '*.styl';
+
+declare namespace NodeJS {
+  interface ProcessEnv {
+    /** NODE 内置环境变量, 会影响到最终构建生成产物 */
+    NODE_ENV: 'development' | 'production',
+    /** 当前构建的平台 */
+    TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'qq' | 'jd' | 'harmony' | 'jdrn'
+    /**
+     * 当前构建的小程序 appid
+     * @description 若不同环境有不同的小程序,可通过在 env 文件中配置环境变量`TARO_APP_ID`来方便快速切换 appid, 而不必手动去修改 dist/project.config.json 文件
+     * @see https://taro-docs.jd.com/docs/next/env-mode-config#特殊环境变量-taro_app_id
+     */
+    TARO_APP_ID: string
+  }
+}
+
+