|
|
@@ -0,0 +1,470 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import { message } from 'antd';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+import { BriefcaseIcon, MapPinIcon, ClockIcon, CurrencyDollarIcon, AcademicCapIcon, CalendarIcon } from '@heroicons/react/24/outline';
|
|
|
+import { useAuth } from '@/client/mobile/hooks/AuthProvider';
|
|
|
+import { jobClient } from '@/client/api';
|
|
|
+import type { CreateJobDto } from '@/server/modules/silver-jobs/job.entity';
|
|
|
+
|
|
|
+interface Company {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface PublishJobFormProps {
|
|
|
+ onSuccess?: () => void;
|
|
|
+ onCancel?: () => void;
|
|
|
+}
|
|
|
+
|
|
|
+const INK_COLORS = {
|
|
|
+ ink: {
|
|
|
+ light: '#f5f3f0',
|
|
|
+ medium: '#d4c4a8',
|
|
|
+ dark: '#8b7355',
|
|
|
+ deep: '#3a2f26',
|
|
|
+ },
|
|
|
+ accent: {
|
|
|
+ red: '#a85c5c',
|
|
|
+ blue: '#4a6b7c',
|
|
|
+ green: '#5c7c5c',
|
|
|
+ },
|
|
|
+ text: {
|
|
|
+ primary: '#2f1f0f',
|
|
|
+ secondary: '#5d4e3b',
|
|
|
+ light: '#8b7355',
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const FONT_STYLES = {
|
|
|
+ title: 'font-serif text-2xl font-bold tracking-wide',
|
|
|
+ sectionTitle: 'font-serif text-lg font-semibold tracking-wide',
|
|
|
+ body: 'font-sans text-base leading-relaxed',
|
|
|
+ caption: 'font-sans text-sm',
|
|
|
+ small: 'font-sans text-xs',
|
|
|
+};
|
|
|
+
|
|
|
+interface FormDataState {
|
|
|
+ companyId: number;
|
|
|
+ title: string;
|
|
|
+ salary: string;
|
|
|
+ educationRequired: string;
|
|
|
+ experienceRequired: string;
|
|
|
+ workTime: string;
|
|
|
+ workLocation: string;
|
|
|
+ description: string;
|
|
|
+ details: string;
|
|
|
+ publishTime: string;
|
|
|
+ endTime: string;
|
|
|
+ status: number;
|
|
|
+}
|
|
|
+
|
|
|
+export const PublishJobForm: React.FC<PublishJobFormProps> = ({ onSuccess, onCancel }) => {
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const { user } = useAuth();
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [companies, setCompanies] = useState<Company[]>([]);
|
|
|
+ const [formData, setFormData] = useState<FormDataState>({
|
|
|
+ companyId: 0,
|
|
|
+ title: '',
|
|
|
+ salary: '',
|
|
|
+ educationRequired: '不限',
|
|
|
+ experienceRequired: '不限',
|
|
|
+ workTime: '',
|
|
|
+ workLocation: '',
|
|
|
+ description: '',
|
|
|
+ details: '',
|
|
|
+ publishTime: new Date().toISOString(),
|
|
|
+ endTime: dayjs().add(30, 'day').toISOString(),
|
|
|
+ status: 1,
|
|
|
+ });
|
|
|
+
|
|
|
+ const educationOptions = ['不限', '初中', '高中', '大专', '本科', '硕士', '博士'];
|
|
|
+ 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('获取企业列表失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fetchCompanies();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleChange = (field: keyof FormDataState, value: string | number) => {
|
|
|
+ setFormData(prev => ({ ...prev, [field]: value }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = async (e: React.FormEvent) => {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ if (!formData.companyId) {
|
|
|
+ message.error('请选择企业');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.title.trim()) {
|
|
|
+ message.error('请输入岗位名称');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.salary.trim()) {
|
|
|
+ message.error('请输入薪资范围');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.workLocation.trim()) {
|
|
|
+ message.error('请输入工作地点');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.description.trim()) {
|
|
|
+ message.error('请输入岗位介绍');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.details.trim()) {
|
|
|
+ message.error('请输入岗位详情');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setLoading(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await jobClient.$post({
|
|
|
+ json: {
|
|
|
+ ...formData,
|
|
|
+ companyId: Number(formData.companyId),
|
|
|
+ publishTime: dayjs(formData.publishTime).toISOString(),
|
|
|
+ endTime: dayjs(formData.endTime).toISOString(),
|
|
|
+ status: 1,
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.status === 201) {
|
|
|
+ message.success('岗位发布成功!');
|
|
|
+ onSuccess?.();
|
|
|
+ // 重置表单
|
|
|
+ setFormData({
|
|
|
+ companyId: 0,
|
|
|
+ title: '',
|
|
|
+ salary: '',
|
|
|
+ educationRequired: '不限',
|
|
|
+ experienceRequired: '不限',
|
|
|
+ workTime: '',
|
|
|
+ workLocation: '',
|
|
|
+ description: '',
|
|
|
+ details: '',
|
|
|
+ publishTime: new Date().toISOString(),
|
|
|
+ endTime: dayjs().add(30, 'day').toISOString(),
|
|
|
+ status: 1,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const errorData = await response.json() as { message?: string; code?: number };
|
|
|
+ message.error(errorData.message || '发布失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发布岗位失败:', error);
|
|
|
+ message.error('发布岗位失败,请稍后重试');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-6">
|
|
|
+ {/* 企业选择 */}
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 选择企业 *
|
|
|
+ </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>
|
|
|
+
|
|
|
+ {/* 岗位名称 */}
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 岗位名称 *
|
|
|
+ </label>
|
|
|
+ <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={formData.title}
|
|
|
+ onChange={(e) => handleChange('title', e.target.value)}
|
|
|
+ placeholder="请输入岗位名称"
|
|
|
+ maxLength={100}
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 薪资范围 */}
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 薪资范围 *
|
|
|
+ </label>
|
|
|
+ <div className="relative">
|
|
|
+ <CurrencyDollarIcon 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={formData.salary}
|
|
|
+ onChange={(e) => handleChange('salary', e.target.value)}
|
|
|
+ placeholder="如:5000-8000元/月"
|
|
|
+ maxLength={50}
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 学历要求和工作经验 */}
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 学历要求
|
|
|
+ </label>
|
|
|
+ <div className="relative">
|
|
|
+ <AcademicCapIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5" style={{ color: INK_COLORS.text.light }} />
|
|
|
+ <select
|
|
|
+ value={formData.educationRequired}
|
|
|
+ onChange={(e) => handleChange('educationRequired', e.target.value)}
|
|
|
+ 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
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {educationOptions.map(option => (
|
|
|
+ <option key={option} value={option}>{option}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 工作经验
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ value={formData.experienceRequired}
|
|
|
+ onChange={(e) => handleChange('experienceRequired', 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
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {experienceOptions.map(option => (
|
|
|
+ <option key={option} value={option}>{option}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 工作时间和工作地点 */}
|
|
|
+ <div className="grid grid-cols-1 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 工作时间 *
|
|
|
+ </label>
|
|
|
+ <div className="relative">
|
|
|
+ <ClockIcon 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={formData.workTime}
|
|
|
+ onChange={(e) => handleChange('workTime', e.target.value)}
|
|
|
+ placeholder="如:朝九晚五,周末双休"
|
|
|
+ maxLength={100}
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 工作地点 *
|
|
|
+ </label>
|
|
|
+ <div className="relative">
|
|
|
+ <MapPinIcon 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={formData.workLocation}
|
|
|
+ onChange={(e) => handleChange('workLocation', e.target.value)}
|
|
|
+ placeholder="请输入详细工作地点"
|
|
|
+ maxLength={200}
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 日期选择 */}
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 发布日期 *
|
|
|
+ </label>
|
|
|
+ <div className="relative">
|
|
|
+ <CalendarIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5" style={{ color: INK_COLORS.text.light }} />
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ value={dayjs(formData.publishTime).format('YYYY-MM-DD')}
|
|
|
+ onChange={(e) => handleChange('publishTime', e.target.value)}
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 截止日期 *
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ value={dayjs(formData.endTime).format('YYYY-MM-DD')}
|
|
|
+ onChange={(e) => handleChange('endTime', e.target.value)}
|
|
|
+ min={dayjs(formData.publishTime).format('YYYY-MM-DD')}
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 岗位介绍 */}
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 岗位介绍 *
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ value={formData.description}
|
|
|
+ onChange={(e) => handleChange('description', e.target.value)}
|
|
|
+ placeholder="简要介绍岗位主要内容..."
|
|
|
+ rows={3}
|
|
|
+ maxLength={500}
|
|
|
+ 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,
|
|
|
+ resize: 'vertical'
|
|
|
+ }}
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 岗位详情 */}
|
|
|
+ <div>
|
|
|
+ <label className={`block mb-2 ${FONT_STYLES.caption} font-medium`} style={{ color: INK_COLORS.text.primary }}>
|
|
|
+ 岗位详情 *
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ value={formData.details}
|
|
|
+ onChange={(e) => handleChange('details', e.target.value)}
|
|
|
+ placeholder="详细描述岗位职责、要求、福利等..."
|
|
|
+ rows={6}
|
|
|
+ maxLength={2000}
|
|
|
+ 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,
|
|
|
+ resize: 'vertical'
|
|
|
+ }}
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 提交按钮 */}
|
|
|
+ <div className="flex space-x-4 pt-4">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={onCancel}
|
|
|
+ className={`flex-1 py-3 rounded-xl border transition-all duration-300 ${FONT_STYLES.body}`}
|
|
|
+ style={{
|
|
|
+ borderColor: INK_COLORS.ink.medium,
|
|
|
+ color: INK_COLORS.text.primary,
|
|
|
+ backgroundColor: 'transparent'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ disabled={loading}
|
|
|
+ className={`flex-1 py-3 rounded-xl transition-all duration-300 ${FONT_STYLES.body} font-medium`}
|
|
|
+ style={{
|
|
|
+ backgroundColor: loading ? INK_COLORS.ink.medium : INK_COLORS.accent.blue,
|
|
|
+ color: 'white',
|
|
|
+ opacity: loading ? 0.7 : 1
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {loading ? '发布中...' : '确认发布'}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default PublishJobForm;
|