123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817 |
- <template>
- <div class="position-create-container">
- <div class="header-container">
- <page-header title="新增职位" @back="goBack" />
- <!-- <div class="header-right">应用至人力资源-对外</div> -->
- </div>
-
- <el-card class="form-card">
- <div class="section-title">
- <div class="section-line"></div>
- <span>职位详情</span>
- </div>
-
- <el-form
- ref="formRef"
- :model="formData"
- :rules="rules"
- label-width="120px"
- label-position="right"
- >
- <el-form-item label="职位名称" prop="title">
- <el-input v-model="formData.title" placeholder="请输入职位名称" maxlength="20" show-word-limit />
- </el-form-item>
-
- <el-form-item label="职位类别" prop="job_category">
- <el-select v-model="formData.job_category" placeholder="请选择职位类别">
- <el-option label="技术" value="技术" />
- <el-option label="产品" value="产品" />
- <el-option label="设计" value="设计" />
- <el-option label="运营" value="运营" />
- <el-option label="市场" value="市场" />
- <el-option label="销售" value="销售" />
- <el-option label="行政" value="行政" />
- <el-option label="财务" value="财务" />
- <el-option label="人力资源" value="人力资源" />
- <el-option label="法务" value="法务" />
- <el-option label="其他" value="其他" />
- </el-select>
- </el-form-item>
-
- <el-form-item label="职位性质" prop="job_type">
- <el-select v-model="formData.job_type" placeholder="请选择职位性质">
- <el-option label="全职" value="0" />
- <el-option label="兼职" value="1" />
- <el-option label="实习" value="2" />
- <el-option label="其他" value="3" />
- </el-select>
- </el-form-item>
-
- <el-form-item label="工作地点" prop="location">
- <el-cascader
- v-model="formData.location"
- :options="locationOptions"
- :props="{
- expandTrigger: 'hover',
- checkStrictly: false,
- value: 'code',
- label: 'name'
- }"
- style="width: 100%"
- placeholder="请选择工作地点"
- clearable
- @change="handleLocationChange"
- />
- </el-form-item>
-
- <el-form-item label="详细地址" prop="detailed_address">
- <el-input v-model="formData.detailed_address" placeholder="请输入详细地址" />
- </el-form-item>
-
- <el-form-item label="福利待遇" prop="benefits">
- <el-select v-model="formData.benefits" placeholder="请选择福利待遇" multiple>
- <el-option label="五险一金" value="五险一金" />
- <el-option label="年终奖" value="年终奖" />
- <el-option label="带薪年假" value="带薪年假" />
- <el-option label="定期体检" value="定期体检" />
- <el-option label="节日福利" value="节日福利" />
- <el-option label="员工旅游" value="员工旅游" />
- <el-option label="弹性工作" value="弹性工作" />
- <el-option label="免费班车" value="免费班车" />
- <el-option label="餐补" value="餐补" />
- <el-option label="交通补贴" value="交通补贴" />
- </el-select>
- </el-form-item>
- <!-- 添加职位薪资字段 -->
- <el-form-item label="职位薪资" prop="salary_range">
- <div class="salary-range-container">
- <el-select
- v-model="formData.salary_type"
- placeholder="薪资类型"
- class="salary-type-select"
- @change="updateSalaryRange"
- >
- <el-option label="月薪" value="月薪" />
- <el-option label="年薪" value="年薪" />
- <el-option label="日薪" value="日薪" />
- <el-option label="时薪" value="时薪" />
- </el-select>
- <el-input-number
- v-model="formData.start_salary"
- placeholder="起始薪资"
- class="salary-input"
- type="number"
- controls-position="right"
- min="0"
- @input="updateSalaryRange"
- @blur="validateSalaryInput"
- />
- <span class="salary-separator">-</span>
- <el-input-number
- v-model="formData.end_salary"
- placeholder="最高薪资"
- class="salary-input"
- controls-position="right"
- type="number"
- min="0"
- @input="updateSalaryRange"
- @blur="validateSalaryInput"
- />
- </div>
- </el-form-item>
-
- <!-- 添加职位条件字段 -->
- <el-form-item label="职位条件">
- <div class="job-conditions-container">
- <el-select v-model="formData.work_experience_required" placeholder="工作经验" class="condition-select">
- <el-option label="不限" value="不限" />
- <el-option label="应届毕业生" value="应届毕业生" />
- <el-option label="1年以下" value="1年以下" />
- <el-option label="1-3年" value="1-3年" />
- <el-option label="3-5年" value="3-5年" />
- <el-option label="5-10年" value="5-10年" />
- <el-option label="10年以上" value="10年以上" />
- </el-select>
- <el-select v-model="formData.education_required" placeholder="学历要求" class="condition-select">
- <el-option label="不限" value="不限" />
- <el-option label="高中" value="高中" />
- <el-option label="大专" value="大专" />
- <el-option label="本科" value="本科" />
- <el-option label="硕士" value="硕士" />
- <el-option label="博士" value="博士" />
- </el-select>
- </div>
- </el-form-item>
-
- <!-- <el-form-item label="胜任力标签" prop="competency_tags">
- <el-select
- v-model="formData.competency_tags"
- multiple
- placeholder="请选择胜任力标签"
- style="width: 100%"
- >
- <el-option
- v-for="tag in competencyTags"
- :key="tag.id"
- :label="tag.name"
- :value="tag.id"
- >
- </el-option>
- </el-select>
- </el-form-item> -->
-
- <!-- <el-form-item label="所属部门" prop="department">
- <el-input v-model="formData.department" placeholder="请输入所属部门" />
- </el-form-item> -->
-
- <el-form-item label="职位状态" prop="status">
- <el-select v-model="formData.status" disabled placeholder="请选择职位状态">
- <el-option label="未发布" :value="0" />
- <el-option label="已发布" :value="1" />
- <!-- <el-option label="已结束" :value="2" /> -->
- </el-select>
- </el-form-item>
-
- <el-form-item label="截止日期" prop="end_date">
- <el-date-picker
- v-model="formData.end_date"
- type="datetime"
- placeholder="请选择截止日期"
- value-format="YYYY-MM-DD HH:mm:ss"
- />
- </el-form-item>
- <el-form-item label="职位要求" prop="requirements">
- <div class="editor-container">
- <div ref="quillEditor" class="quill-editor"></div>
- </div>
- </el-form-item>
-
- <!-- 添加职位描述字段 -->
- <el-form-item label="职位描述" prop="description">
- <div class="editor-container">
- <div ref="descriptionEditor" class="quill-editor"></div>
- </div>
- </el-form-item>
-
-
-
- <el-form-item>
- <el-button type="primary" @click="submitForm">保存</el-button>
- <el-button @click="goBack">取消</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
- import { useRouter } from 'vue-router';
- import { ElMessage } from 'element-plus';
- import * as api from '../list/api';
- import type { FormInstance } from 'element-plus';
- import Quill from 'quill';
- import 'quill/dist/quill.snow.css'; // 引入样式
- import axios from 'axios';
- import { getPCAData } from '../../../utils/pcaData';
- import { Session } from '/@/utils/storage';
- // 添加防抖函数
- const debounce = (fn: Function, delay: number) => {
- let timer: any = null;
- return (...args: any[]) => {
- if (timer) clearTimeout(timer);
- timer = setTimeout(() => {
- fn(...args);
- }, delay);
- };
- };
- const router = useRouter();
- const formRef = ref<FormInstance>();
- // 添加胜任力标签接口定义
- interface CompetencyTag {
- id: number;
- name: string;
- description: string;
- }
- // 在 PositionFormData 接口中修改 location 字段类型
- interface PositionFormData {
- title: string;
- job_category: string;
- job_type: string;
- city: string;
- location: string[]; // 存储选中的代码
- location_names?: string[]; // 存储选中的名称,设为可选
- detailed_address: string;
- benefits: string[];
- requirements: string;
- /* job_type?: number; */
- salary_type: string;
- start_salary: string;
- end_salary: string;
- work_experience_required: string;
- education_required: string;
- salary_range?: string;
- department?: string;
- end_date?: string;
- description?: string;
- province: string;
- district: string;
- location_str?: string;
- status: number;
- competency_tags: number[];
- }
- // 表单数据中添加 location_names 字段
- const formData = reactive<PositionFormData>({
- title: '',
- job_category: '',
- job_type: '',
- city: '',
- location: [],
- location_names: [], // 新增字段
- detailed_address: '',
- benefits: [],
- requirements: '',
- /* job_type: 0, */
- salary_type: '月薪',
- start_salary: '',
- end_salary: '',
- work_experience_required: '',
- education_required: '',
- salary_range: '',
- department: '',
- end_date: '',
- description: '',
- province: '',
- district: '',
- location_str: '',
- status: 0,
- competency_tags: [],
- });
- // 富文本内容验证函数
- const validateRichText = (rule: any, value: string, callback: any) => {
- if (!value) {
- callback(new Error(rule.message));
- return;
- }
-
- // 去除HTML标签并检查是否有实际内容
- const textContent = value.replace(/<[^>]*>/g, '').trim();
- if (!textContent) {
- callback(new Error(rule.message));
- return;
- }
-
- callback();
- };
- // 薪资范围验证函数
- const validateSalaryRange = (rule: any, value: any, callback: any) => {
- if (!formData.start_salary || !formData.end_salary) {
- callback(new Error('请完整填写起始薪资和最高薪资'));
- return;
- }
-
- const startSalary = Number(formData.start_salary);
- const endSalary = Number(formData.end_salary);
-
- if (isNaN(startSalary) || isNaN(endSalary)) {
- callback(new Error('薪资必须为有效数字'));
- return;
- }
-
- if (startSalary < 0 || endSalary < 0) {
- callback(new Error('薪资不能为负数'));
- return;
- }
-
- if (startSalary > endSalary) {
- callback(new Error('起始薪资不能高于最高薪资'));
- return;
- }
-
- callback();
- };
- // 表单验证规则
- const rules = {
- title: [{ required: true, message: '职位名称必填', trigger: 'blur' }],
- job_category: [{ required: true, message: '职位类别必填', trigger: 'change' }],
- job_type: [{ required: true, message: '职位性质必填', trigger: 'change' }],
- location: [{ required: true, message: '工作地点必填', trigger: 'change' }],
- detailed_address: [{ required: true, message: '详细地址必填', trigger: 'blur' }],
- department: [{ required: true, message: '所属部门必填', trigger: 'blur' }],
- status: [{ required: true, message: '职位状态必填', trigger: 'change' }],
- end_date: [{ required: true, message: '截止日期必填', trigger: 'change' }],
- requirements: [{
- required: true,
- message: '职位要求必填',
- validator: validateRichText,
- trigger: 'blur'
- }],
- description: [{
- required: true,
- message: '职位描述必填',
- validator: validateRichText,
- trigger: 'blur'
- }],
- salary_range: [{ required: true, validator: validateSalaryRange, trigger: 'blur' }],
- /* competency_tags: [
- { required: true, message: '请选择胜任力标签', trigger: 'change' }
- ], */
- };
- // 添加对描述编辑器的引用
- const quillEditor = ref<HTMLElement | null>(null);
- const descriptionEditor = ref<HTMLElement | null>(null);
- let editor: any = null;
- let descEditor: any = null;
- // 工作地点选项
- const locationOptions = getPCAData();
- // 修改处理地点选择变化的函数
- const handleLocationChange = (value: any) => {
- console.log('地址选择变化:', value);
- if (value && Array.isArray(value) && value.length > 0) {
- const [provinceCode, cityCode, districtCode] = value;
- const province = locationOptions.find(p => p.code === provinceCode);
- let city, district;
- if (province && province.children) {
- city = province.children.find(c => c.code === cityCode);
- if (city && city.children) {
- district = city.children.find(d => d.code === districtCode);
- }
- }
- // 保存名称
- formData.location_names = [
- province?.name || '',
- city?.name || '',
- district?.name || ''
- ].filter(Boolean);
- // 更新各个字段
- formData.province = province?.name || '';
- formData.city = city?.name || '';
- formData.district = district?.name || '';
- formData.location_str = formData.location_names.join('/');
- } else {
- formData.location_names = [];
- formData.province = '';
- formData.city = '';
- formData.district = '';
- formData.location_str = '';
- }
- };
- // 实时更新薪资范围
- const updateSalaryRange = (isBlur = false) => {
- // 验证薪资范围
- if (formData.start_salary && formData.end_salary) {
- const startSalary = Number(formData.start_salary);
- const endSalary = Number(formData.end_salary);
-
- // 如果起始薪资大于最高薪资,自动调整
- if (startSalary > endSalary) {
- formData.end_salary = formData.start_salary;
- if (isBlur) {
- ElMessage.warning('起始薪资不能高于最高薪资,已自动调整为相同值');
- } else {
- ElMessage.warning('起始薪资不能高于最高薪资,已自动调整');
- }
- }
- }
-
- if (formData.salary_type && formData.start_salary && formData.end_salary) {
- formData.salary_range = `${formData.salary_type} ${formData.start_salary}-${formData.end_salary}`;
- } else {
- formData.salary_range = '';
- }
- };
- // 失焦时的薪资调整处理
- const handleSalaryBlur = () => {
- // 检查是否需要调整薪资范围
- if (formData.start_salary && formData.end_salary) {
- const startSalary = Number(formData.start_salary);
- const endSalary = Number(formData.end_salary);
-
- // 如果起始薪资大于最高薪资,自动调整
- if (startSalary > endSalary) {
- formData.end_salary = formData.start_salary;
- ElMessage.warning('起始薪资不能高于最高薪资,已自动调整为相同值');
- }
- }
-
- // 更新薪资范围显示
- updateSalaryRange();
-
- // 触发表单验证
- if (formRef.value) {
- formRef.value.validateField('salary_range');
- }
- };
- // 验证薪资输入(保持向后兼容)
- const validateSalaryInput = () => {
- handleSalaryBlur();
- };
- // 添加生成职位描述的方法
- const generatePositionDescription = async () => {
- // 检查必要字段是否已填写
- if (!formData.title || !formData.work_experience_required ||
- !formData.education_required || !formData.salary_range) {
- return;
- }
- try {
- // 调用生成描述接口
- const response = await api.GeneratePositionDescription({
- position_title: formData.title,
- experience_req: formData.work_experience_required,
- education_req: formData.education_required,
- salary_range: formData.salary_range,
- tenant_id:Session.get('tenant_id')
- });
- if (response.code === 2000 && response.data) {
- // 格式化返回的数据
- const formattedDescription = `
- <ul>
- ${response.data.responsibilities.map((item: string) => `<li>${item}</li>`).join('\n')}
- </ul>
- <p><strong>经验要求:</strong>${response.data.experience}</p>
- <p><strong>学历要求:</strong>${response.data.education}</p>
- <ul>
- ${response.data.skills.map((item: string) => `<li>${item}</li>`).join('\n')}
- </ul>
- <ul>
- ${response.data.qualities.map((item: string) => `<li>${item}</li>`).join('\n')}
- </ul>
- <ul>
- ${response.data.bonus.map((item: string) => `<li>${item}</li>`).join('\n')}
- </ul>`;
- // 更新编辑器内容
- if (descEditor) {
- descEditor.clipboard.dangerouslyPasteHTML(formattedDescription);
- formData.description = formattedDescription;
- }
- }
- } catch (error) {
- console.error('生成职位描述失败:', error);
- ElMessage.error('生成职位描述失败,请稍后重试');
- }
- };
- // 创建防抖后的生成函数
- const debouncedGenerateDescription = debounce(generatePositionDescription, 1000);
- // 监听相关字段变化
- watch(
- () => [
- formData.title,
- formData.work_experience_required,
- formData.education_required,
- formData.salary_range
- ],
- () => {
- debouncedGenerateDescription();
- },
- { deep: true }
- );
- // 在 setup 中添加胜任力相关的响应式数据
- const competencyTags = ref<CompetencyTag[]>([]);
- const competencyDescriptions = ref<Record<number, string>>({});
- // 添加获取胜任力标签的方法
- const fetchCompetencyTags = async () => {
- try {
- const response = await api.GetCompetencyList({page:1,pageSize:1000});
- console.log('response', response);
- competencyTags.value = response.data.items;
- // 构建描述映射
- /* competencyTags.value.forEach(tag => {
- competencyDescriptions.value[tag.id] = tag.description;
- }); */
- } catch (error) {
- console.error('获取胜任力标签失败:', error);
- ElMessage.error('获取胜任力标签失败');
- }
- };
- onMounted(() => {
- // 初始化职位要求编辑器
- if (quillEditor.value) {
- editor = new Quill(quillEditor.value, {
- theme: 'snow',
- modules: {
- toolbar: [
- ['bold', 'italic', 'underline', 'strike'],
- ['blockquote', 'code-block'],
- [{ 'header': 1 }, { 'header': 2 }],
- [{ 'list': 'ordered' }, { 'list': 'bullet' }],
- [{ 'script': 'sub' }, { 'script': 'super' }],
- [{ 'indent': '-1' }, { 'indent': '+1' }],
- [{ 'direction': 'rtl' }],
- [{ 'size': ['small', false, 'large', 'huge'] }],
- [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
- [{ 'color': [] }, { 'background': [] }],
- [{ 'font': [] }],
- [{ 'align': [] }],
- ['clean']
- ]
- },
- placeholder: '请输入职位要求'
- });
-
- // 设置初始内容
- if (formData.requirements) {
- editor.clipboard.dangerouslyPasteHTML(formData.requirements);
- }
-
- // 监听内容变化
- editor.on('text-change', () => {
- formData.requirements = editor.root.innerHTML;
- // 触发表单验证
- nextTick(() => {
- formRef.value?.validateField('requirements');
- });
- });
- }
-
- // 初始化职位描述编辑器
- if (descriptionEditor.value) {
- descEditor = new Quill(descriptionEditor.value, {
- theme: 'snow',
- modules: {
- toolbar: [
- ['bold', 'italic', 'underline', 'strike'],
- ['blockquote', 'code-block'],
- [{ 'header': 1 }, { 'header': 2 }],
- [{ 'list': 'ordered' }, { 'list': 'bullet' }],
- [{ 'script': 'sub' }, { 'script': 'super' }],
- [{ 'indent': '-1' }, { 'indent': '+1' }],
- [{ 'direction': 'rtl' }],
- [{ 'size': ['small', false, 'large', 'huge'] }],
- [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
- [{ 'color': [] }, { 'background': [] }],
- [{ 'font': [] }],
- [{ 'align': [] }],
- ['clean']
- ]
- },
- placeholder: '请输入职位描述'
- });
-
- // 设置初始内容
- if (formData.description) {
- descEditor.clipboard.dangerouslyPasteHTML(formData.description);
- }
-
- // 监听内容变化
- descEditor.on('text-change', () => {
- formData.description = descEditor.root.innerHTML;
- // 触发表单验证
- nextTick(() => {
- formRef.value?.validateField('description');
- });
- });
- }
-
- // 在 onMounted 中调用获取胜任力标签
- fetchCompetencyTags();
- });
- onBeforeUnmount(() => {
- editor = null;
- descEditor = null;
- });
- // 返回列表页
- const goBack = () => {
- router.push('/position/list');
- };
- // 修改提交表单函数
- const submitForm = async () => {
- if (!formRef.value) return;
-
- // 确保获取最新内容
- if (editor) {
- formData.requirements = editor.root.innerHTML;
- }
-
- if (descEditor) {
- formData.description = descEditor.root.innerHTML;
- }
-
- // 确保薪资范围最新
- updateSalaryRange();
-
- await formRef.value.validate(async (valid: boolean, fields: any) => {
- if (valid) {
- try {
- const formToSubmit = { ...formData };
-
- // 处理地址信息 - 只提交名称
- if (formToSubmit.location_names && formToSubmit.location_names.length > 0) {
- formToSubmit.province = formToSubmit.location_names[0];
- formToSubmit.city = formToSubmit.location_names[1] || '';
- formToSubmit.district = formToSubmit.location_names[2] || '';
- formToSubmit.location = formToSubmit.location_names; // 使用名称数组替换代码数组
- formToSubmit.location_str = formToSubmit.location_names.join('/');
- }
-
- // 创建新对象,排除 location_names 字段
- const {location_names, ...submitData} = formToSubmit;
-
- const response = await api.AddObj(submitData);
- ElMessage.success('职位添加成功');
- router.push(`/position/detail?id=${response.data.id}`);
- } catch (error) {
- console.error('保存失败:', error);
- ElMessage.error('保存失败,请重试');
- }
- } else {
- console.log('验证失败:', fields);
- }
- });
- };
- </script>
- <style scoped>
- .position-create-container {
- padding: 20px;
- }
- .form-card {
- margin: 0 auto;
- margin-top: 20px;
- width: 40%;
- }
- .section-title {
- display: flex;
- align-items: center;
- margin-bottom: 20px;
- font-weight: bold;
- color: #333;
- }
- .section-line {
- width: 4px;
- height: 16px;
- background-color: #67C23A;
- margin-right: 8px;
- }
- .editor-container {
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- }
- .editor-toolbar {
- display: flex;
- padding: 8px;
- border-bottom: 1px solid #dcdfe6;
- background-color: #f5f7fa;
- }
- .toolbar-btn {
- margin-right: 5px;
- padding: 4px 8px;
- background: none;
- border: none;
- cursor: pointer;
- border-radius: 3px;
- }
- .toolbar-btn:hover {
- background-color: #e6e6e6;
- }
- .toolbar-btn.active {
- background-color: #ffecd1;
- }
- .toolbar-select {
- margin-right: 5px;
- padding: 4px;
- border: 1px solid #dcdfe6;
- border-radius: 3px;
- }
- .header-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .header-right {
- color: #909399;
- font-size: 14px;
- }
- .editable-content {
- min-height: 200px;
- padding: 10px;
- border: 1px solid #dcdfe6;
- border-top: none;
- overflow-y: auto;
- background-color: #fff;
- }
- .quill-editor {
- height: 300px;
- }
- .salary-range-container {
- display: flex;
- align-items: center;
- }
- .salary-type-select {
- width: 100%;
- min-width: 120px;
- margin-right: 10px;
- }
- .salary-input {
- width: 100%;
- min-width: 90px;
- }
- .salary-separator {
- margin: 0 10px;
- }
- .job-conditions-container {
- display: flex;
- gap: 20px;
- }
- .condition-select {
- width: 100%;
- min-width: 150px;
- }
- </style>
|