Bläddra i källkod

✨ feat(job): 实现认证企业搜索选择功能

- 添加认证企业API调用,支持分页和关键词搜索
- 实现企业搜索框带防抖功能的实时搜索
- 优化企业选择UI,使用下拉列表替代传统select
- 添加企业搜索空状态提示和加载状态

📝 docs(api): 添加认证公司测试数据文档

- 创建insert-certified-companies.md文件
- 提供5家认证公司的SQL插入语句
- 添加API测试、curl测试和前端验证步骤
- 说明预期结果和验证方法
yourname 7 månader sedan
förälder
incheckning
3efbed2eac

+ 62 - 0
docs/insert-certified-companies.md

@@ -0,0 +1,62 @@
+# 插入已认证公司测试数据
+
+## SQL插入语句
+
+请在数据库中执行以下SQL语句,插入测试用的已认证公司数据:
+
+```sql
+-- 插入已认证公司测试数据
+INSERT INTO silver_companies (
+  name, short_name, industry_category, company_type, employee_count,
+  address, longitude, latitude, contact_person, contact_phone,
+  is_certified, user_id, view_count, favorite_count
+) VALUES
+  ('北京银龄科技有限公司', '银龄科技', '科技服务', '民营企业', '50-100人',
+   '北京市朝阳区科技园区88号A座', 116.4074, 39.9042, '张经理', '13888888888',
+   1, 1001, 156, 23),
+  ('上海智慧养老服务有限公司', '智慧养老', '养老服务', '有限公司', '100-500人',
+   '上海市浦东新区张江高科技园区999号', 121.4737, 31.2304, '李总监', '13999999999',
+   1, 1002, 89, 12),
+  ('广州乐龄人力资源有限公司', '乐龄人力', '人力资源', '股份有限公司', '200-1000人',
+   '广州市天河区珠江新城888号', 113.2644, 23.1291, '王主管', '13777777777',
+   1, 1003, 234, 45),
+  ('深圳颐养天年健康管理有限公司', '颐养天年', '健康管理', '有限公司', '50-200人',
+   '深圳市南山区科技园777号', 113.9448, 22.5431, '刘经理', '13666666666',
+   1, 1004, 167, 34),
+  ('成都银发经济发展有限公司', '银发经济', '经济发展', '民营企业', '100-500人',
+   '成都市高新区天府软件园666号', 104.0668, 30.5728, '陈总监', '13555555555',
+   1, 1005, 98, 18);
+
+-- 验证插入的数据
+SELECT id, name, is_certified, created_at FROM silver_companies WHERE is_certified = 1;
+```
+
+## 测试验证
+
+### 1. 直接API测试
+访问以下URL测试认证公司API:
+```
+https://your-domain.com/api/v1/silver-companies/certified?page=1&pageSize=100
+```
+
+### 2. 使用curl测试
+```bash
+curl -X GET 'https://d8d-ai-vscode-8080-152-136-template-9-group.r.d8d.fun/api/v1/silver-companies/certified?page=1&pageSize=100'
+```
+
+### 3. 前端验证
+1. 打开银龄岗管理页面
+2. 点击"新建岗位"按钮
+3. 在公司名称下拉框中应该看到上述5家已认证公司
+4. 可以搜索如"银龄"、"科技"等关键词进行筛选
+
+## 预期结果
+
+执行上述插入语句后,在前端将看到:
+- 北京银龄科技有限公司
+- 上海智慧养老服务有限公司
+- 广州乐龄人力资源有限公司
+- 深圳颐养天年健康管理有限公司
+- 成都银发经济发展有限公司
+
+这些公司已通过认证(is_certified=1),可以在新建岗位时直接选择使用。

+ 0 - 0
docs/insert-certified-companies.sql


+ 125 - 35
src/client/mobile/components/PublishJobForm.tsx

@@ -1,11 +1,12 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { message } from 'antd';
 import { message } from 'antd';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import { BriefcaseIcon, MapPinIcon, ClockIcon, CurrencyDollarIcon, AcademicCapIcon, CalendarIcon } from '@heroicons/react/24/outline';
 import { BriefcaseIcon, MapPinIcon, ClockIcon, CurrencyDollarIcon, AcademicCapIcon, CalendarIcon } from '@heroicons/react/24/outline';
 import { useAuth } from '@/client/mobile/hooks/AuthProvider';
 import { useAuth } from '@/client/mobile/hooks/AuthProvider';
-import { jobClient } from '@/client/api';
+import { jobClient, silverCompaniesClient } from '@/client/api';
 import type { CreateJobDto } from '@/server/modules/silver-jobs/job.entity';
 import type { CreateJobDto } from '@/server/modules/silver-jobs/job.entity';
+import type { InferResponseType } from 'hono/client';
 
 
 interface Company {
 interface Company {
   id: number;
   id: number;
@@ -64,6 +65,10 @@ export const PublishJobForm: React.FC<PublishJobFormProps> = ({ onSuccess, onCan
   const { user } = useAuth();
   const { user } = useAuth();
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [companies, setCompanies] = useState<Company[]>([]);
   const [companies, setCompanies] = useState<Company[]>([]);
+  const [searchKeyword, setSearchKeyword] = useState('');
+  const [loadingCompanies, setLoadingCompanies] = useState(false);
+  const [showCompanyDropdown, setShowCompanyDropdown] = useState(false);
+  const [debounceTimer, setDebounceTimer] = useState<NodeJS.Timeout | null>(null);
   const [formData, setFormData] = useState<FormDataState>({
   const [formData, setFormData] = useState<FormDataState>({
     companyId: 0,
     companyId: 0,
     title: '',
     title: '',
@@ -82,29 +87,85 @@ export const PublishJobForm: React.FC<PublishJobFormProps> = ({ onSuccess, onCan
   const educationOptions = ['不限', '初中', '高中', '大专', '本科', '硕士', '博士'];
   const educationOptions = ['不限', '初中', '高中', '大专', '本科', '硕士', '博士'];
   const experienceOptions = ['不限', '1年以下', '1-3年', '3-5年', '5-10年', '10年以上'];
   const experienceOptions = ['不限', '1年以下', '1-3年', '3-5年', '5-10年', '10年以上'];
 
 
-  // 获取企业列表
-  useEffect(() => {
-    const fetchCompanies = async () => {
-      try {
-        // 模拟获取企业数据 - 实际项目中应从API获取
-        setCompanies([
-          { id: 1, name: '北京科技有限公司' },
-          { id: 2, name: '上海互联网公司' },
-          { id: 3, name: '广州教育科技公司' },
-        ]);
-      } catch (error) {
-        console.error('获取企业列表失败:', error);
-        message.error('获取企业列表失败');
+  // 获取认证企业列表
+  const fetchCertifiedCompanies = useCallback(async (keyword?: string) => {
+    try {
+      setLoadingCompanies(true);
+      const response = await silverCompaniesClient.certified.$get({
+        query: {
+          page: 1,
+          pageSize: 50,
+          ...(keyword ? { keyword } : {})
+        }
+      });
+      
+      if (response.status === 200) {
+        const data = await response.json();
+        setCompanies(data.data.map(company => ({
+          id: company.id,
+          name: company.name
+        })));
       }
       }
-    };
-
-    fetchCompanies();
+    } catch (error) {
+      console.error('获取认证企业列表失败:', error);
+      message.error('获取认证企业列表失败');
+    } finally {
+      setLoadingCompanies(false);
+    }
   }, []);
   }, []);
 
 
+  useEffect(() => {
+    fetchCertifiedCompanies();
+  }, [fetchCertifiedCompanies]);
+
   const handleChange = (field: keyof FormDataState, value: string | number) => {
   const handleChange = (field: keyof FormDataState, value: string | number) => {
     setFormData(prev => ({ ...prev, [field]: value }));
     setFormData(prev => ({ ...prev, [field]: value }));
   };
   };
 
 
+  const handleCompanySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const keyword = e.target.value;
+    setSearchKeyword(keyword);
+    
+    if (debounceTimer) {
+      clearTimeout(debounceTimer);
+    }
+    
+    const timer = setTimeout(() => {
+      fetchCertifiedCompanies(keyword || undefined);
+    }, 300);
+    
+    setDebounceTimer(timer);
+    setShowCompanyDropdown(true);
+  };
+
+  const handleCompanySelect = (company: Company) => {
+    setFormData(prev => ({ ...prev, companyId: company.id }));
+    setSearchKeyword(company.name);
+    setShowCompanyDropdown(false);
+  };
+
+  // 点击外部关闭下拉框
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      const target = event.target as HTMLElement;
+      if (!target.closest('.company-select-container')) {
+        setShowCompanyDropdown(false);
+      }
+    };
+
+    document.addEventListener('mousedown', handleClickOutside);
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside);
+      if (debounceTimer) {
+        clearTimeout(debounceTimer);
+      }
+    };
+  }, [debounceTimer]);
+
+  const filteredCompanies = companies.filter(company =>
+    company.name.toLowerCase().includes(searchKeyword.toLowerCase())
+  );
+
   const handleSubmit = async (e: React.FormEvent) => {
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
     
     
@@ -184,26 +245,55 @@ export const PublishJobForm: React.FC<PublishJobFormProps> = ({ onSuccess, onCan
   return (
   return (
     <form onSubmit={handleSubmit} className="space-y-6">
     <form onSubmit={handleSubmit} className="space-y-6">
       {/* 企业选择 */}
       {/* 企业选择 */}
-      <div>
+      <div className="relative company-select-container">
         <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
         <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
           选择企业 *
           选择企业 *
         </label>
         </label>
-        <select
-          value={formData.companyId}
-          onChange={(e) => handleChange('companyId', parseInt(e.target.value))}
-          className={`w-full px-4 py-3 rounded-xl border transition-all duration-300 ${FONT_STYLES.body}`}
-          style={{ 
-            backgroundColor: 'rgba(255,255,255,0.7)',
-            borderColor: INK_COLORS.ink.medium,
-            color: INK_COLORS.text.primary
-          }}
-          required
-        >
-          <option value={0}>请选择企业</option>
-          {companies.map(company => (
-            <option key={company.id} value={company.id}>{company.name}</option>
-          ))}
-        </select>
+        <div className="relative">
+          <BriefcaseIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5" style={{ color: INK_COLORS.text.light }} />
+          <input
+            type="text"
+            value={searchKeyword}
+            onChange={handleCompanySearch}
+            onFocus={() => setShowCompanyDropdown(true)}
+            placeholder="请输入企业名称搜索"
+            className={`w-full pl-10 pr-4 py-3 rounded-xl border transition-all duration-300 ${FONT_STYLES.body}`}
+            style={{
+              backgroundColor: 'rgba(255,255,255,0.7)',
+              borderColor: INK_COLORS.ink.medium,
+              color: INK_COLORS.text.primary
+            }}
+            required
+          />
+          {loadingCompanies && (
+            <div className="absolute right-3 top-1/2 transform -translate-y-1/2">
+              <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[#8b7355]"></div>
+            </div>
+          )}
+        </div>
+        
+        {showCompanyDropdown && filteredCompanies.length > 0 && (
+          <div className="absolute z-10 w-full mt-1 bg-white border border-[#d4c4a8] rounded-xl shadow-lg max-h-60 overflow-y-auto">
+            {filteredCompanies.map(company => (
+              <div
+                key={company.id}
+                onClick={() => handleCompanySelect(company)}
+                className="px-4 py-3 hover:bg-[#f5f3f0] cursor-pointer transition-colors duration-200"
+                style={{ color: INK_COLORS.text.primary }}
+              >
+                {company.name}
+              </div>
+            ))}
+          </div>
+        )}
+        
+        {showCompanyDropdown && filteredCompanies.length === 0 && !loadingCompanies && (
+          <div className="absolute z-10 w-full mt-1 bg-white border border-[#d4c4a8] rounded-xl shadow-lg p-4 text-center" style={{ color: INK_COLORS.text.secondary }}>
+            未找到匹配的企业
+          </div>
+        )}
+        
+        <input type="hidden" value={formData.companyId} name="companyId" required />
       </div>
       </div>
 
 
       {/* 岗位名称 */}
       {/* 岗位名称 */}