|
|
@@ -0,0 +1,576 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import { PhotoIcon, UserIcon, PhoneIcon, EnvelopeIcon } from '@heroicons/react/24/outline';
|
|
|
+import { silverUsersClient } from '@/client/api';
|
|
|
+import { fileClient } from '@/client/api';
|
|
|
+import { compressImage } from '@/client/utils/upload';
|
|
|
+
|
|
|
+interface PublishTalentFormProps {
|
|
|
+ onSuccess?: () => void;
|
|
|
+ onCancel?: () => void;
|
|
|
+ initialData?: any;
|
|
|
+}
|
|
|
+
|
|
|
+interface FormData {
|
|
|
+ realName: string;
|
|
|
+ nickname?: string;
|
|
|
+ organization?: string;
|
|
|
+ age: number;
|
|
|
+ gender: 'MALE' | 'FEMALE' | 'OTHER';
|
|
|
+ phone: string;
|
|
|
+ email?: string;
|
|
|
+ personalIntro?: string;
|
|
|
+ personalSkills?: string;
|
|
|
+ personalExperience?: string;
|
|
|
+ jobSeekingRequirements?: string;
|
|
|
+ avatarUrl?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface FormState {
|
|
|
+ loading: boolean;
|
|
|
+ submitting: boolean;
|
|
|
+ errors: Record<string, string>;
|
|
|
+ previewImage: string;
|
|
|
+}
|
|
|
+
|
|
|
+export default function PublishTalentForm({ onSuccess, onCancel, initialData }: PublishTalentFormProps) {
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const [formData, setFormData] = useState<FormData>({
|
|
|
+ realName: '',
|
|
|
+ nickname: '',
|
|
|
+ organization: '',
|
|
|
+ age: 60,
|
|
|
+ gender: 'MALE',
|
|
|
+ phone: '',
|
|
|
+ email: '',
|
|
|
+ personalIntro: '',
|
|
|
+ personalSkills: '',
|
|
|
+ personalExperience: '',
|
|
|
+ jobSeekingRequirements: '',
|
|
|
+ avatarUrl: ''
|
|
|
+ });
|
|
|
+
|
|
|
+ const [formState, setFormState] = useState<FormState>({
|
|
|
+ loading: false,
|
|
|
+ submitting: false,
|
|
|
+ errors: {},
|
|
|
+ previewImage: ''
|
|
|
+ });
|
|
|
+
|
|
|
+ // 从localStorage恢复草稿
|
|
|
+ useEffect(() => {
|
|
|
+ const savedDraft = localStorage.getItem('silverTalentDraft');
|
|
|
+ if (savedDraft && !initialData) {
|
|
|
+ try {
|
|
|
+ const draft = JSON.parse(savedDraft);
|
|
|
+ setFormData(prev => ({ ...prev, ...draft }));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to parse draft:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, [initialData]);
|
|
|
+
|
|
|
+ // 自动保存草稿
|
|
|
+ useEffect(() => {
|
|
|
+ const timer = setTimeout(() => {
|
|
|
+ if (!formState.submitting) {
|
|
|
+ localStorage.setItem('silverTalentDraft', JSON.stringify(formData));
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ return () => clearTimeout(timer);
|
|
|
+ }, [formData, formState.submitting]);
|
|
|
+
|
|
|
+ const validateField = (name: string, value: any): string => {
|
|
|
+ const rules: Record<string, any[]> = {
|
|
|
+ realName: [
|
|
|
+ { required: true, message: '请输入真实姓名' },
|
|
|
+ { min: 1, max: 50, message: '姓名长度应在1-50字符之间' }
|
|
|
+ ],
|
|
|
+ age: [
|
|
|
+ { required: true, message: '请输入年龄' },
|
|
|
+ { min: 50, max: 100, message: '年龄应在50-100岁之间' }
|
|
|
+ ],
|
|
|
+ gender: [
|
|
|
+ { required: true, message: '请选择性别' }
|
|
|
+ ],
|
|
|
+ phone: [
|
|
|
+ { required: true, message: '请输入联系电话' },
|
|
|
+ { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
|
|
|
+ ],
|
|
|
+ email: [
|
|
|
+ { type: 'email', message: '请输入正确的邮箱地址' }
|
|
|
+ ],
|
|
|
+ nickname: [
|
|
|
+ { max: 50, message: '昵称长度不能超过50字符' }
|
|
|
+ ],
|
|
|
+ organization: [
|
|
|
+ { max: 255, message: '机构名称长度不能超过255字符' }
|
|
|
+ ],
|
|
|
+ personalIntro: [
|
|
|
+ { max: 1000, message: '个人简介不能超过1000字' }
|
|
|
+ ],
|
|
|
+ personalSkills: [
|
|
|
+ { max: 2000, message: '个人技能描述不能超过2000字' }
|
|
|
+ ],
|
|
|
+ personalExperience: [
|
|
|
+ { max: 3000, message: '个人经历不能超过3000字' }
|
|
|
+ ],
|
|
|
+ jobSeekingRequirements: [
|
|
|
+ { max: 1000, message: '求职需求不能超过1000字' }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ const fieldRules = rules[name];
|
|
|
+ if (!fieldRules) return '';
|
|
|
+
|
|
|
+ for (const rule of fieldRules) {
|
|
|
+ if (rule.required && !value) {
|
|
|
+ return rule.message;
|
|
|
+ }
|
|
|
+ if (rule.min !== undefined && value < rule.min) {
|
|
|
+ return rule.message;
|
|
|
+ }
|
|
|
+ if (rule.max !== undefined) {
|
|
|
+ if (typeof value === 'string' && value.length > rule.max) {
|
|
|
+ return rule.message;
|
|
|
+ }
|
|
|
+ if (typeof value === 'number' && value > rule.max) {
|
|
|
+ return rule.message;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (rule.pattern && !rule.pattern.test(value)) {
|
|
|
+ return rule.message;
|
|
|
+ }
|
|
|
+ if (rule.type === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
|
+ return rule.message;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleInputChange = (name: string, value: string | number) => {
|
|
|
+ setFormData(prev => ({ ...prev, [name]: value }));
|
|
|
+
|
|
|
+ // 实时验证
|
|
|
+ const error = validateField(name, value);
|
|
|
+ setFormState(prev => ({
|
|
|
+ ...prev,
|
|
|
+ errors: { ...prev.errors, [name]: error }
|
|
|
+ }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const validateForm = (): boolean => {
|
|
|
+ const errors: Record<string, string> = {};
|
|
|
+ let isValid = true;
|
|
|
+
|
|
|
+ const fields = ['realName', 'age', 'gender', 'phone', 'email', 'nickname', 'organization', 'personalIntro', 'personalSkills', 'personalExperience', 'jobSeekingRequirements'];
|
|
|
+
|
|
|
+ fields.forEach(key => {
|
|
|
+ const value = formData[key as keyof FormData];
|
|
|
+ const error = validateField(key, value);
|
|
|
+ if (error) {
|
|
|
+ errors[key] = error;
|
|
|
+ isValid = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ setFormState(prev => ({ ...prev, errors }));
|
|
|
+ return isValid;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleImageUpload = async (file: File) => {
|
|
|
+ if (file.size > 2 * 1024 * 1024) {
|
|
|
+ setFormState(prev => ({ ...prev, errors: { ...prev.errors, avatarUrl: '图片大小不能超过2MB' } }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
|
|
|
+ setFormState(prev => ({ ...prev, errors: { ...prev.errors, avatarUrl: '请上传JPG、PNG或WebP格式的图片' } }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setFormState(prev => ({ ...prev, loading: true }));
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 压缩图片
|
|
|
+ const compressedFile = await compressImage(file, 800, 800, 0.8);
|
|
|
+
|
|
|
+ // 使用FileReader预览图片,实际项目中应使用真实的文件上传API
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ const imageUrl = e.target?.result as string;
|
|
|
+ setFormData(prev => ({ ...prev, avatarUrl: imageUrl }));
|
|
|
+ setFormState(prev => ({ ...prev, previewImage: imageUrl, errors: { ...prev.errors, avatarUrl: '' } }));
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(compressedFile);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Upload failed:', error);
|
|
|
+ setFormState(prev => ({ ...prev, errors: { ...prev.errors, avatarUrl: '图片上传失败,请重试' } }));
|
|
|
+ } finally {
|
|
|
+ setFormState(prev => ({ ...prev, loading: false }));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = async (e: React.FormEvent) => {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ if (!validateForm()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setFormState(prev => ({ ...prev, submitting: true }));
|
|
|
+
|
|
|
+ try {
|
|
|
+ const requestData = {
|
|
|
+ realName: formData.realName,
|
|
|
+ nickname: formData.nickname || undefined,
|
|
|
+ organization: formData.organization || undefined,
|
|
|
+ age: Number(formData.age),
|
|
|
+ gender: formData.gender === 'MALE' ? 1 : formData.gender === 'FEMALE' ? 2 : 3,
|
|
|
+ phone: formData.phone,
|
|
|
+ email: formData.email || undefined,
|
|
|
+ personalIntro: formData.personalIntro || undefined,
|
|
|
+ personalSkills: formData.personalSkills || undefined,
|
|
|
+ personalExperience: formData.personalExperience || undefined,
|
|
|
+ jobSeekingRequirements: formData.jobSeekingRequirements || undefined,
|
|
|
+ avatarUrl: formData.avatarUrl || undefined
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await silverUsersClient.profiles.$post({
|
|
|
+ json: requestData
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ localStorage.removeItem('silverTalentDraft');
|
|
|
+ onSuccess?.();
|
|
|
+ navigate('/silver-talents');
|
|
|
+ } else {
|
|
|
+ const error = await response.json();
|
|
|
+ setFormState(prev => ({ ...prev, errors: { ...prev.errors, submit: error.message || '提交失败,请重试' } }));
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Submit failed:', error);
|
|
|
+ setFormState(prev => ({ ...prev, errors: { ...prev.errors, submit: '网络错误,请检查网络连接' } }));
|
|
|
+ } finally {
|
|
|
+ setFormState(prev => ({ ...prev, submitting: false }));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const clearDraft = () => {
|
|
|
+ localStorage.removeItem('silverTalentDraft');
|
|
|
+ setFormData({
|
|
|
+ realName: '',
|
|
|
+ nickname: '',
|
|
|
+ organization: '',
|
|
|
+ age: 60,
|
|
|
+ gender: 'MALE',
|
|
|
+ phone: '',
|
|
|
+ email: '',
|
|
|
+ personalIntro: '',
|
|
|
+ personalSkills: '',
|
|
|
+ personalExperience: '',
|
|
|
+ jobSeekingRequirements: '',
|
|
|
+ avatarUrl: ''
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderCharCount = (text?: string, maxLength?: number) => (
|
|
|
+ <span className="text-sm text-gray-500">
|
|
|
+ {(text || '').length}/{maxLength || 0}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+
|
|
|
+ return (
|
|
|
+ <form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-2 sm:p-4">
|
|
|
+ <div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
|
+ {/* 基本信息区域 */}
|
|
|
+ <div className="p-6">
|
|
|
+ <h3 className="text-lg font-medium text-gray-900 mb-4">基本信息</h3>
|
|
|
+
|
|
|
+ {/* 头像上传 */}
|
|
|
+ <div className="mb-6">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">个人头像</label>
|
|
|
+ <div className="flex items-center space-x-4">
|
|
|
+ <div className="relative">
|
|
|
+ {formState.previewImage || formData.avatarUrl ? (
|
|
|
+ <img
|
|
|
+ src={formState.previewImage || formData.avatarUrl}
|
|
|
+ alt="头像预览"
|
|
|
+ className="w-20 h-20 rounded-full object-cover border-2 border-gray-200"
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div className="w-20 h-20 rounded-full bg-gray-100 flex items-center justify-center">
|
|
|
+ <UserIcon className="w-8 h-8 text-gray-400" />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ accept="image/*"
|
|
|
+ onChange={(e) => e.target.files?.[0] && handleImageUpload(e.target.files[0])}
|
|
|
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
|
+ disabled={formState.loading}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <p className="text-sm text-gray-600">点击上传头像</p>
|
|
|
+ <p className="text-xs text-gray-400">支持 JPG、PNG、WebP,最大2MB</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {formState.errors.avatarUrl && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.avatarUrl}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 姓名 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 真实姓名 <span className="text-red-500">*</span>
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={formData.realName}
|
|
|
+ onChange={(e) => handleInputChange('realName', e.target.value)}
|
|
|
+ className="w-full px-3 py-3 md:py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-base md:text-sm"
|
|
|
+ placeholder="请输入真实姓名"
|
|
|
+ />
|
|
|
+ {formState.errors.realName && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.realName}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 年龄 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 年龄 <span className="text-red-500">*</span>
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ value={formData.age}
|
|
|
+ onChange={(e) => handleInputChange('age', parseInt(e.target.value))}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ >
|
|
|
+ {Array.from({ length: 51 }, (_, i) => i + 50).map(age => (
|
|
|
+ <option key={age} value={age}>{age}岁</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ {formState.errors.age && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.age}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 性别 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 性别 <span className="text-red-500">*</span>
|
|
|
+ </label>
|
|
|
+ <div className="flex space-x-4">
|
|
|
+ {[
|
|
|
+ { value: 'MALE', label: '男' },
|
|
|
+ { value: 'FEMALE', label: '女' },
|
|
|
+ { value: 'OTHER', label: '其他' }
|
|
|
+ ].map(option => (
|
|
|
+ <label key={option.value} className="flex items-center">
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="gender"
|
|
|
+ value={option.value}
|
|
|
+ checked={formData.gender === option.value}
|
|
|
+ onChange={(e) => handleInputChange('gender', e.target.value)}
|
|
|
+ className="mr-2"
|
|
|
+ />
|
|
|
+ <span className="text-sm text-gray-700">{option.label}</span>
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ {formState.errors.gender && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.gender}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 联系电话 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 联系电话 <span className="text-red-500">*</span>
|
|
|
+ </label>
|
|
|
+ <div className="relative">
|
|
|
+ <PhoneIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
|
+ <input
|
|
|
+ type="tel"
|
|
|
+ value={formData.phone}
|
|
|
+ onChange={(e) => handleInputChange('phone', e.target.value)}
|
|
|
+ className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请输入手机号码"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {formState.errors.phone && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.phone}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 昵称 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">昵称</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={formData.nickname}
|
|
|
+ onChange={(e) => handleInputChange('nickname', e.target.value)}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请输入昵称(选填)"
|
|
|
+ />
|
|
|
+ {formState.errors.nickname && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.nickname}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 所属机构 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">所属机构</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={formData.organization}
|
|
|
+ onChange={(e) => handleInputChange('organization', e.target.value)}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="如退休单位、社团等(选填)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 邮箱 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
|
|
+ <div className="relative">
|
|
|
+ <EnvelopeIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
|
+ <input
|
|
|
+ type="email"
|
|
|
+ value={formData.email}
|
|
|
+ onChange={(e) => handleInputChange('email', e.target.value)}
|
|
|
+ className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请输入邮箱地址(选填)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {formState.errors.email && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.email}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 个人展示区域 */}
|
|
|
+ <div className="p-6 border-t border-gray-200">
|
|
|
+ <h3 className="text-lg font-medium text-gray-900 mb-4">个人展示</h3>
|
|
|
+
|
|
|
+ {/* 个人简介 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 个人简介
|
|
|
+ <span className="ml-2 text-sm text-gray-500">
|
|
|
+ {renderCharCount(formData.personalIntro, 1000)}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ value={formData.personalIntro}
|
|
|
+ onChange={(e) => handleInputChange('personalIntro', e.target.value)}
|
|
|
+ rows={4}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请简要介绍自己,包括专业背景、兴趣爱好等"
|
|
|
+ />
|
|
|
+ {formState.errors.personalIntro && (
|
|
|
+ <p className="mt-1 text-sm text-red-600">{formState.errors.personalIntro}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 个人技能 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 个人技能
|
|
|
+ <span className="ml-2 text-sm text-gray-500">
|
|
|
+ {renderCharCount(formData.personalSkills, 2000)}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ value={formData.personalSkills}
|
|
|
+ onChange={(e) => handleInputChange('personalSkills', e.target.value)}
|
|
|
+ rows={3}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请输入您的专业技能,用逗号分隔多个技能,如:书法,国画,古典文学"
|
|
|
+ />
|
|
|
+ <p className="mt-1 text-sm text-gray-500">多个技能请用逗号分隔</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 个人经历 */}
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 个人经历
|
|
|
+ <span className="ml-2 text-sm text-gray-500">
|
|
|
+ {renderCharCount(formData.personalExperience, 3000)}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ value={formData.personalExperience}
|
|
|
+ onChange={(e) => handleInputChange('personalExperience', e.target.value)}
|
|
|
+ rows={5}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请输入您的工作经历、教育背景、获得荣誉等"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 求职需求区域 */}
|
|
|
+ <div className="p-6 border-t border-gray-200">
|
|
|
+ <h3 className="text-lg font-medium text-gray-900 mb-4">求职需求</h3>
|
|
|
+
|
|
|
+ <div className="mb-4">
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
+ 求职需求
|
|
|
+ <span className="ml-2 text-sm text-gray-500">
|
|
|
+ {renderCharCount(formData.jobSeekingRequirements, 1000)}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ value={formData.jobSeekingRequirements}
|
|
|
+ onChange={(e) => handleInputChange('jobSeekingRequirements', e.target.value)}
|
|
|
+ rows={4}
|
|
|
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ placeholder="请输入您的求职需求,包括期望工作类型、时间安排、薪资待遇等"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 错误提示 */}
|
|
|
+ {formState.errors.submit && (
|
|
|
+ <div className="p-4 bg-red-50 border-t border-red-200">
|
|
|
+ <p className="text-sm text-red-600">{formState.errors.submit}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 操作按钮 */}
|
|
|
+ <div className="p-6 bg-gray-50 border-t border-gray-200">
|
|
|
+ <div className="flex space-x-4">
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ disabled={formState.submitting || formState.loading}
|
|
|
+ className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
|
+ >
|
|
|
+ {formState.submitting ? '提交中...' : '发布人才信息'}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={clearDraft}
|
|
|
+ className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
|
|
+ >
|
|
|
+ 清空草稿
|
|
|
+ </button>
|
|
|
+ {onCancel && (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={onCancel}
|
|
|
+ className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ );
|
|
|
+}
|