lifanagju_citu 1 ヶ月 前
コミット
4706ceda55

+ 67 - 0
api/new/position.js

@@ -26,6 +26,32 @@ export const getUnpaidOrder = (params) => {
   })
 }
 
+// 招聘端-发布职位详情
+export const getJobDetails = (params) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-advertised/detail',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: true,
+      openEncryption: true,
+      auth: true
+    }
+  })
+}
+
+// 获取招聘职位扩展信息
+export const getJobAdvertisedExtend = (jobId) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-advertised/get/extend?jobId=' + jobId,
+    method: 'GET',
+    custom: {
+      showLoading: true,
+      auth: true
+    }
+  })
+}
+
 // 创建订单
 export const createTradeOrder = (data) => {
   return request({
@@ -116,3 +142,44 @@ export const getEnterprisePubJobTypePermission = () => {
     }
   })
 }
+
+// 求职端-发布职位-获取职位模板
+export const getRecruitPositionDetails = async (id) => {
+  return request({
+    url: `/app-api/menduner/system/recruit/position/get?id=${id}`,
+    method: 'GET',
+    custom: {
+      showLoading: true,
+      auth: false
+    }
+  })
+}
+
+// 招聘端-发布职位
+export const saveJobAdvertised = (data) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-advertised/save',
+    method: "POST",
+    data,
+    custom: {
+      openEncryption: true,
+      showLoading: true,
+      auth: true
+    }
+  })
+}
+
+// 保存招聘职位扩展信息
+export const saveJobAdvertisedExtend = (data) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/job-advertised/save/extend',
+    method: "POST",
+    data,
+    custom: {
+      openEncryption: true,
+      showLoading: true,
+      auth: true
+    }
+  })
+}
+

+ 16 - 3
api/resume.js

@@ -51,7 +51,7 @@ export const getResumeTrainExp = async () => {
   })
 }
 
-// // 获取-教育经历
+// 获取-教育经历
 export const getResumeEduExp = async () => {
   return request({
     url: '/app-api/menduner/system/person/resume/get/edu/exp',
@@ -199,7 +199,7 @@ export const deleteResumeJobInterested = async (id) => {
   })
 }
 
-// // 获取求职意向
+// 获取求职意向
 export const getResumeJobInterested = async () => {
   return request({
     url: '/app-api/menduner/system/person/resume/get/job/interested',
@@ -211,7 +211,7 @@ export const getResumeJobInterested = async () => {
   })
 }
 
-// // 根据专业名称模糊搜索
+// 根据专业名称模糊搜索
 export const schoolMajorByName = async (params) => {
   return request({
     url: '/app-api/menduner/system/major/search/by/name',
@@ -224,6 +224,19 @@ export const schoolMajorByName = async (params) => {
   })
 }
 
+// 根据专业id搜索
+export const schoolMajorById = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/major/get',
+    params,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
 // 根据学校名称模糊搜索
 export const schoolSearchByName = async (params) => {
   return request({

+ 1 - 0
components/PositionList/index.vue

@@ -71,6 +71,7 @@
         @close="handleClose"
       ></uni-popup-dialog>
     </uni-popup>
+    <!-- 支付 -->
     <payPopup v-if="props.payable" ref="payRef" amount="123" @paySuccess="paySuccess"></payPopup>
   </view>
 </template>

+ 5 - 5
components/payPopup/index.vue

@@ -52,17 +52,13 @@ import { onHide, onShow } from '@dcloudio/uni-app'
 // import { userStore } from '@/store/user'; const useUserStore = userStore()
 import { getSocialUser, socialUserBind, payOrderSubmit, getOrderPayStatus, getEnableCodeList } from '@/api/common'
 import { createTradeOrder, getUnpaidOrder } from '@/api/new/position'
-const emit = defineEmits(['paySuccess'])
+const emit = defineEmits(['paySuccess', 'close'])
 
 const props = defineProps({
   title: { // 标题
     type: String,
     default: '支付'
   },
-  amount: { // 支付金额
-    type: [String, Number],
-    default: '金额获取失败'
-  },
 })
 
 const payType = [
@@ -116,10 +112,14 @@ const popup = ref()
 const handleClose = () => {
   tabBarShow(true)
   popup.value.close()
+  emit('close')
 }
+
 const query = ref(null)
+const amount = ref('') // 支付金额
 const handleOpen = (val) => {
   query.value = val
+  amount.value = Number(val?.price) ? Number(val?.price)/100 : 0
   tabBarShow(false)
   popup.value.open('bottom')
 }

+ 104 - 22
components/positionAdd/components/baseInfo.vue

@@ -1,11 +1,10 @@
-<!--  -->
 <template>
 	<view class="f-straight wrapper">
 		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px" label-align="right">
 			<uni-forms-item label="职位类型" name="positionId" required>
         <view class="positionTemplate">
           <uni-data-picker class="picker" popup-title="请选择职位类型" v-model="formData.positionId" :localdata="dictObj?.positionTreeData || []" :clear-icon="false" :map="{ text: 'nameCn', value: 'id'}"></uni-data-picker>
-          <button class="btn" type="primary" size="mini" @click="null">职位模板</button>
+          <button v-if="formData.positionId" class="btn" type="primary" size="mini" @click="useJobTemplate">职位模板</button>
         </view>
 			</uni-forms-item>
 			<uni-forms-item required label="职位名称" name="name">
@@ -13,10 +12,10 @@
 			</uni-forms-item>
       <uni-forms-item label="到期时间" name="expireTime" required>
 				<view class="d-flex">
-          <picker mode="date" :value="formData.expireTime" :disabled="expireTimeDisabled" fields="month" :start="startDate" @change="expireTimeChange">
+          <picker mode="date" :value="formData.expireTime" :disabled="expireTimeDisabled" :start="startDate" @change="expireTimeChange">
             <view class="uni-input ss-m-t-20" :style="{'opacity': expireTimeDisabled ? '0.5' : '1'}">{{ formData.expireTime }}</view>
           </picker>
-          <uni-data-checkbox selectedColor="#00B760" class="ss-m-l-50 ss-m-t-14" multiple v-model="sofar" :localdata="[{ text: '长期有效', value: 1 }]" @change="handleChangeSofar"></uni-data-checkbox>
+          <uni-data-checkbox selectedColor="#00B760" class="ss-m-l-50 ss-m-t-14" multiple v-model="soFar" :localdata="[{ text: '长期有效', value: 1 }]" @change="handleChangeSofar"></uni-data-checkbox>
         </view>
 			</uni-forms-item>
       <uni-forms-item label="岗位职责" name="content" required>
@@ -27,24 +26,45 @@
         <uni-easyinput type="textarea" v-model="formData.requirement" autoHeight  placeholder="请输入内容"></uni-easyinput>
 			</uni-forms-item>
 		</uni-forms>
+    <!-- 确认框 -->
+    <uni-popup ref="confirm" type="dialog">
+      <uni-popup-dialog
+        type="warn"
+        cancelText="取消"
+        confirmText="确认" 
+        title="系统提示"
+        content="您确定要放弃目前岗位描述的内容吗?"
+        @confirm="handleConfirm"
+        @close="handleClose"
+      ></uni-popup-dialog>
+    </uni-popup>
 	</view>
 </template>
 <script setup>
-import { ref } from 'vue'
+import { ref, unref } from 'vue'
 import { dictObj } from '@/utils/position.js'
 import { getNextDate } from '@/utils/date'
+import { getRecruitPositionDetails } from '@/api/new/position'
+import { dateToTimestamp } from '@/utils/date.js'
 // import RichEditor from '@/components/RichEditor'
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => {}
+  }
+})
+
 const formData = ref({
-  positionName: '',
-  positionId: '',
-  expireTime: getNextDate(15, 'YYYY-MM-DD', 'day'),
-  name: '',
+  positionId: props.data?.positionId || '',
+  // name: props.data?.name || '',
+  name: props.data?.name + new Date().getTime().toString() || '',
+  expireTime: props.data?.expireTime || getNextDate(15, 'YYYY-MM-DD', 'day'),
+  content: props.data?.content || '',
+  requirement: props.data?.requirement || ''
 })
+
 const startDate = new Date().getFullYear() + '-' + (new Date().getMonth() + 1) // 不可选时间
-const sofar = ref([])
-const expireTimeDisabled = ref(false)
-// expireTimeDisabled.value = true
-    // sofar.value = [1]
+const expireTimeDisabled = ref(props.data?.expireTime === null ? true : false)
 // 至今
 const handleChangeSofar = (e) => {
   const value = e.detail.value.length ? e.detail.value[0] : ''
@@ -72,25 +92,87 @@ const rules = {
 	},
 }
 
+const pushTemplate = () => {
+  formData.value.content = jobTemplateRes.value.content
+  formData.value.requirement = jobTemplateRes.value.requirement
+  uni.showToast({ title: '模板填充完成!', icon: 'success' })
+}
+
+const confirm = ref()
+const handleClose = () => {
+  confirm.value.close()
+}
+const handleConfirm = () => {
+  try {
+    uni.showLoading({ title: '替换中...', mask: true })
+    pushTemplate()
+  } catch (error) {
+    uni.showToast({ title: '替换失败', icon: 'error' })
+    console.log(error)
+  } finally {
+    uni.hideLoading()
+  }
+}
+
+const jobTemplateRes = ref({})
+const useJobTemplate = async () => {
+  if (!formData.value.positionId) return Snackbar.warning('请先选择职位类型')
+  // 获取职位模板内容-赋值
+  const res = await getRecruitPositionDetails(formData.value.positionId)
+  if (!res?.data || !res.data .content || !res.data .requirement) {
+    uni.showToast({ title: '此职位类型没有可使用的模板!', icon: 'none', duration: 2000 })
+    return
+  }
+  jobTemplateRes.value = res.data
+  console.log('res.data:', res.data)
+  if (formData.value?.content || formData.value?.requirement) {
+    // 弹窗提示
+    confirm.value.open()
+  } else {
+    // 无内容点击默认填充
+    pushTemplate()
+  }
+}
+
 // const richEditorBlur = (content) => {
 //   formData.value.content = content
 // }
+
+const form = ref()
+const soFar = ref(props.data?.expireTime === null ? [1] : [])
+const getQuery = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+  const obj = {
+    hirePrice: 0,
+    soFar: Boolean(soFar.value?.length),
+    hire: false,
+    ...formData.value
+  }
+  obj.expireTime = obj.soFar ? null : dateToTimestamp(obj.expireTime)
+  obj && Object.keys(obj).length && Object.keys(obj).forEach(key => { if (['areaId', 'eduType', 'expType'].includes(key) && obj[key] === -1)  obj[key] = null })
+
+  console.log('基本信息:', obj)
+  return obj
+}
+
+defineExpose({
+  getQuery
+})
+
 </script>
 <style lang="scss" scoped>
 .positionTemplate {
-  display: flex;
- .picker {
-  flex: 1;
-  margin-right: 5px;
- }
+  text-align: right;
  .btn {
   width: 90px;
   line-height: 34px;
+  margin-top: 10px;
  }
 }
 
-// :deep(.uni-forms-item__content) {
-// 	max-width: 100%;
-// 	overflow: hidden;
-// }
+:deep(.uni-forms-item__content) {
+	max-width: 100%;
+	overflow: hidden;
+}
 </style>

+ 92 - 0
components/positionAdd/components/extend.vue

@@ -0,0 +1,92 @@
+<!--  -->
+<template>
+	<view class="f-straight wrapper">
+		<uni-forms ref="form" :modelValue="formData" validateTrigger="bind" label-width="90px" label-align="right">
+			<uni-forms-item label="招聘部门" name="dept">
+        <uni-easyinput v-model="formData.dept" placeholder="请填写招聘部门"></uni-easyinput>
+			</uni-forms-item>
+			<uni-forms-item label="专业要求" name="majorName">
+				<uni-combox placeholder="请选择专业要求" v-model="majorName" :candidates="majorData" @input="handleSearchMajor"></uni-combox>
+			</uni-forms-item>
+			<uni-forms-item label="工作频率" name="dateType">
+				<uni-data-picker popup-title="请选择工作频率" v-model="formData.dateType" :localdata="dateTypeArr" :clear-icon="false" :map="{ text: 'label', value: 'value'}"></uni-data-picker>
+			</uni-forms-item>
+			<uni-forms-item label="出勤天数" name="frequendayay">
+        <uni-number-box v-model="formData.day" :width="100"></uni-number-box>
+			</uni-forms-item>
+		</uni-forms>
+	</view>
+</template>
+<script setup>
+import { ref } from 'vue'
+import { schoolMajorByName, schoolMajorById } from '@/api/resume'
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const formData = ref({
+  dept: props.data?.dept || '',
+  dateType: props.data?.frequency?.dateType || '',
+  day: props.data?.frequency?.day-0 || 0,
+})
+
+const dateTypeArr = [
+  { label: '每周', value: 'week' },
+  { label: '每月', value: 'month' },
+  { label: '每年', value: 'year' }
+]
+
+const majorData = ref([])
+const majorDataClone = ref([])
+const majorName = ref(props.data?.major || '')
+const handleSearchMajor = (e) => {
+  if (!e) return majorData.value = []
+  schoolMajorByName({ name: e }).then(res => {
+    const list = res?.data && res.data?.length ? res.data : []
+    setMajorData(list)
+    if (!list?.length) {
+      // majorName.value = ''
+      uni.showToast({ title: '未找到相关专业,请重新输入', icon: 'none', duration: 2000 })
+    }
+  })
+}
+function setMajorData (list) {
+  majorData.value = list.map(e => e.nameCn)
+  majorDataClone.value = list
+}
+// if (majorName.value) handleSearchMajor(majorName.value)
+
+async function getMajorById (id) {
+  if (!id) return
+  const { data } = await schoolMajorById({ id })
+  if (!data) return
+  majorName.value = data.nameCn
+  setMajorData([data])
+}
+if (props.data?.majorId) getMajorById(props.data.majorId)
+
+const form = ref()
+const getQuery = () => {
+  const obj = {
+    dept: formData.value.dept,
+    frequency: {
+      dateType: formData.value.dateType,
+      day: formData.value.day
+    },
+    major:  majorDataClone.value?.length ? majorName.value : '',
+    majorId:  majorDataClone.value?.length ? majorDataClone.value.find(e => e.nameCn === majorName.value)?.id : ''
+  }
+  console.log('扩展信息:', obj)
+  return obj
+}
+
+defineExpose({
+  getQuery
+})
+
+</script>
+<style lang="scss" scoped>
+</style>

+ 236 - 0
components/positionAdd/components/portrait.vue

@@ -0,0 +1,236 @@
+<template>
+  <uni-popup ref="popup" :is-mask-click="true" borderRadius="10px 10px 0 0" background-color="#fff" >
+      <view class="popup-content">
+        <view class="popup-content-close">
+          <view class="icon" @tap="handleClose">
+            <uni-icons
+              type="closeempty"
+              color="#999"
+              size="20"
+            />
+          </view>
+        </view>
+        <view class="popup-content-main">
+          <view class="box">
+            <!-- 已选中 -->
+            <view class="chose borderLine">
+              <view class="choseTitle">已选中关键字:</view>
+              <view class="tags">
+                <view
+                  v-for="tag in select"
+                  :key="tag"
+                  class="tag"
+                  style="color: orange; border: 2rpx solid orange;"
+                  @tap="handleCancelSelect(tag)"
+                >
+                  {{ tag }}
+                  <uni-icons type="clear" size="16" color="#ea8d03"></uni-icons>
+                </view>
+              </view>
+            </view>
+            <!-- 选择项 -->
+            <view v-if="showTagList && tagList?.length" class="list">
+              <uni-collapse v-model="collapseOpen">
+                <uni-collapse-item
+                  v-for="val in tagList" :key="val.id"
+                  :name="val.id"
+                  :title="val?.nameCn || '--'"
+                >
+                  <view v-if="val?.children?.length" class="tags">
+                    <view v-for="k in val.children" :key="k.id">
+                      <view v-if="select.includes(k.nameCn)" class="tag" style="color: grey; border: 2rpx solid grey;">
+                        <uni-icons type="plusempty" size="14" color="#00B760"></uni-icons>
+                        {{ k?.nameCn || '--' }}
+                      </view>
+                      <view v-else class="tag" style="color: #00B760; border: 2rpx solid #00B760;" @tap="handleSelect(k.nameCn)">
+                        <uni-icons type="plusempty" size="14" color="#00B760"></uni-icons>
+                        {{ k?.nameCn || '--' }}
+                      </view>
+                    </view>
+                  </view>
+                </uni-collapse-item>
+              </uni-collapse>
+            </view>
+            <view class="f-horizon-center">
+              <button type="primary" size="default" class="send-button"  @click="submit">提 交</button>
+            </view>
+          </view>
+        </view>
+      </view>
+    </uni-popup>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { getTagTreeDataApi } from '@/api/user'
+const emit = defineEmits(['submit'])
+
+const popup = ref()
+const handleClose = () => {
+  popup.value.close()
+}
+const handleOpen = () => {
+  popup.value.open('bottom')
+}
+
+// 选择
+const handleSelect = (nameCn) => {
+  const result = select.value.includes(nameCn)
+  if (!result) return select.value.push(nameCn)
+  else select.value = select.value.filter(e => e !== nameCn)
+}
+
+// 删除
+const handleCancelSelect = (nameCn) => {
+  select.value = select.value.filter(e => e !== nameCn)
+}
+
+// // 获取基础信息
+// function getBaseInfo () {
+//   const baseInfo = useUserStore.baseInfo
+//   select.value = baseInfo.tagList &&  baseInfo.tagList?.length ? baseInfo.tagList : []
+// }
+// getBaseInfo()
+
+const select = ref([])
+const tagList = ref([])
+const collapseOpen = ref([])
+const showTagList = ref(false)
+// 获取标签字典数据
+const getTagList = async () => {
+  showTagList.value = false
+  const res = await getTagTreeDataApi({ type: 2 })
+  const data = res?.data?.length ? res.data : []
+  // collapseOpen.value = data.map(e => e.id)
+  tagList.value = data
+  showTagList.value = true
+}
+getTagList()
+
+// 提交
+const submit = () => {
+  emit('submit', select.value)
+  handleClose()
+}
+
+defineExpose({
+  handleOpen
+})
+</script>
+
+<style lang="scss" scoped>
+$px: 30rpx;
+.borderLine {
+  border-bottom: 2rpx solid #f5f5f5;
+}
+.box {
+  padding: 20rpx $px;
+  .chose {
+    margin-bottom: $px;
+    .choseTitle {
+      margin-bottom: $px;
+    }
+  }
+  .tags {
+    padding: $px 0;
+    display: flex;
+    flex-wrap: wrap;
+    .tag {
+      margin: 0 15rpx 12rpx 0;
+      border: 2rpx 15rpx #00B760;
+      color: #00B760;
+      white-space: nowrap;
+      padding: 4rpx 10rpx;
+      border-radius: 10rpx;
+      font-size: 24rpx;
+    }
+  }
+}
+
+.popup-content {
+  z-index: 999;
+  max-height: 500px;
+  display: flex;
+  flex-direction: column;
+  &-close {
+    display: flex;
+    padding: 10px;
+    justify-content: flex-end;
+    .icon {
+      width: 30px;
+      height: 30px;
+      background: #ccc;
+      border-radius: 30px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+  &-main {
+    flex: 1;
+    height: 0;
+    overflow-y: auto;
+    &-count {
+      margin-bottom: 20px;
+      text-align: center;
+      .title {
+        font-size: 28rpx;
+        color: #666
+      }
+      .pay {
+        font-size: 52rpx;
+        color: #000;
+        font-weight: 600;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 10px 0;
+      }
+    }
+    &-type {
+      width: 100%;
+      padding: 0 20px;
+      box-sizing: border-box;
+      .card {
+        border-radius: 10px;
+        margin: 0 auto;
+        background: #FFF;
+        padding: 10px;
+        &-label {
+          padding: 15px 0;
+          box-sizing: border-box;
+          display: flex;
+          justify-content: space-between;
+          border-bottom: 1px solid #eee;
+          &:last-of-type {
+            border-bottom: none;
+          }
+          .name {
+            display: flex;
+            align-items: center;
+            color: #333;
+          }
+        }
+      }
+    }
+  }
+  &-btn {
+    height: 70px;
+    width: 100%;
+    margin-top: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    &-s {
+      height: 40px;
+      width: 75%;
+      line-height: 40px;
+      color: #FFF;
+      // color: #724d2b;
+      background: #00B760;
+      // background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+      border-radius: 90px;
+    }
+  }
+}
+</style>

+ 162 - 87
components/positionAdd/components/requirement.vue

@@ -3,7 +3,7 @@
 	<view class="f-straight wrapper">
 		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px" label-align="right">
 			<uni-forms-item label="招聘类型" name="type" required>
-				<uni-data-picker popup-title="请选择招聘类型" v-model="formData.type" :localdata="jobType" :clear-icon="false" :map="{ text: 'key', value: 'value'}"></uni-data-picker>
+				<uni-data-picker popup-title="请选择招聘类型" v-model="formData.type" :localdata="jobType" :clear-icon="false" :map="{ text: 'key', value: 'value'}" @change="typeChange"></uni-data-picker>
 			</uni-forms-item>
 			<uni-forms-item label="学历要求" name="eduType" required>
 				<uni-data-picker popup-title="请选择学历要求" v-model="formData.eduType" :localdata="dictObj?.edu || []" :clear-icon="false" :map="{ text: 'label', value: 'value'}"></uni-data-picker>
@@ -11,17 +11,17 @@
 			<uni-forms-item label="工作经验" name="expType" required>
 				<uni-data-picker popup-title="请选择工作经验" v-model="formData.expType" :localdata="dictObj?.exp || []" :clear-icon="false" :map="{ text: 'label', value: 'value'}"></uni-data-picker>
 			</uni-forms-item>
-      <uni-forms-item label="最低薪资" name="payFrom" required label-width="90px">
+      <uni-forms-item label="最低薪资" name="payFrom" :required="Boolean(!salary?.length)" label-width="90px">
 				<view class="d-flex">
-          <uni-number-box v-model="formData.payFrom" :disabled="salary?.length" :min="1" :max="999999999" :step="1000" :width="100" @change="payChange"></uni-number-box>
-          <uni-data-checkbox v-model="salary" multiple :localdata="[{ text: '薪资面议', value: 1 }]" selectedColor="#00B760" class="ss-m-l-20" @change="salaryChange"></uni-data-checkbox>
+          <uni-number-box v-model="formData.payFrom" :disabled="Boolean(salary?.length)" :min="1" :max="999999999" :step="salaryStep" :width="100" @change="payChange"></uni-number-box>
+          <uni-data-checkbox v-model="salary" multiple :localdata="[{ text: '薪资面议', value: 1 }]" selectedColor="#00B760" class="ss-m-l-20" ></uni-data-checkbox>
         </view>
 			</uni-forms-item>
-      <uni-forms-item label="最高薪资" name="payTo" required label-width="90px">
-        <uni-number-box v-model="formData.payTo" :disabled="salary?.length" :min="payToMin" :max="999999999" :step="1000" :width="100"></uni-number-box>
+      <uni-forms-item label="最高薪资" name="payTo" :required="Boolean(!salary?.length)" label-width="90px">
+        <uni-number-box v-model="formData.payTo" :disabled="Boolean(salary?.length)" :min="payToMin" :max="999999999" :step="salaryStep" :width="100"></uni-number-box>
 			</uni-forms-item>
-			<uni-forms-item label="计薪时段" name="payUnit" required>
-				<uni-data-picker popup-title="请选择计薪时段" v-model="formData.payUnit" :orderby="salary?.length" :localdata="dictObj?.payUnit || []" :clear-icon="false" :map="{ text: 'label', value: 'value'}"></uni-data-picker>
+			<uni-forms-item label="计薪时段" name="payUnit" :required="Boolean(!salary?.length)">
+				<uni-data-picker popup-title="请选择计薪时段" :placeholder="Boolean(salary?.length) ? '薪资面议' : '请选择'" v-model="formData.payUnit" :readonly="Boolean(salary?.length)" :localdata="dictObj?.payUnit || []" :clear-icon="false" :map="{ text: 'label', value: 'value'}"></uni-data-picker>
 			</uni-forms-item>
       <uni-forms-item label="工作城市" name="areaId" required label-width="90px">
 				<uni-data-picker popup-title="请选择工作城市" v-model="formData.areaId" :localdata="dictObj?.areaTreeData || []" :clear-icon="false" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
@@ -29,26 +29,54 @@
 			<uni-forms-item required label="详情地址" name="address">
         <uni-easyinput v-model="formData.address" placeholder="请填写详细地址"></uni-easyinput>
 			</uni-forms-item>
+			<uni-forms-item label="职位关键字" name="tagList">
+        <view class="tagsBox" @tap="portraitOpen">
+          <view class="tags" v-if="tagList?.length">
+            <view
+              v-for="tag in tagList"
+              :key="tag"
+              class="tag"
+            >
+              {{ tag }}
+            </view>
+          </view>
+          <uni-icons
+            type="icon-Edit"
+            color="#00B760"
+            class="ss-m-t-20"
+            custom-prefix="iconfont"
+            size="18"
+          ></uni-icons>
+        </view>
+			</uni-forms-item>
 		</uni-forms>
+    <portrait ref="portraitRef" @submit="portraitSubmit"></portrait>
 	</view>
 </template>
 <script setup>
-import { ref } from 'vue'
+import { ref, unref } from 'vue'
 import { dictObj } from '@/utils/position.js'
 import { getEnterprisePubJobTypePermission } from '@/api/new/position'
-
-console.log('dictObj:', dictObj)
+import portrait from './portrait.vue'
+const emit = defineEmits(['requireTypeChange'])
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => {}
+  }
+})
 
 const formData = ref({
-  type: '',
-  eduType: '',
-  expType: '',
-  payFrom: '',
-  payTo: '',
-  payUnit: '',
-  areaId: '',
-  address: '',
+  type: props.data?.type || '',
+  eduType: props.data?.eduType || '',
+  expType: props.data?.expType || '',
+  payFrom: props.data?.payFrom || '',
+  payTo: props.data?.payTo || '',
+  payUnit: props.data?.payUnit || '',
+  areaId: props.data?.areaId || '',
+  address: props.data?.address || '',
 })
+const tagList = ref(props.data?.tagList?.length ? props.data.tagList : [])
 
 const rules = {
 	type:{
@@ -60,59 +88,59 @@ const rules = {
 	expType:{
 		rules: [{required: true, errorMessage: '请选择工作经验' }]
 	},
-	// payFrom:{
-	// 	rules: salary.value?.length ? [] : [
-  //     {
-  //       required: true,
-  //       errorMessage: '请填写最低薪资',
-  //     },
-  //     {
-  //       validateFunction: function (rule, value, data, callback) {
-  //         if (value < 1) {
-  //           callback('数额不得小于1')
-  //         }
-  //         return true
-  //       }
-  //     },
-  //     {
-  //       validateFunction: function (rule, value, data, callback) {
-  //         if (!formData.value?.payTo || (Number(value) < formData.value.payTo ? Number(formData.value.payTo) : 0)) {
-  //           return true
-  //         } else {
-  //           callback('应低于最高薪资')
-  //         }
-  //       }
-  //     }
-  //   ]
-	// },
-	// payTo:{
-	// 	rules:  salary.value?.length ? [] : [
-  //     {
-  //       required: true,
-  //       errorMessage: '请填写最高薪资',
-  //     },
-  //     {
-  //       validateFunction: function (rule, value, data, callback) {
-  //         if (value < 1) {
-  //           callback('数额不得小于1')
-  //         }
-  //         return true
-  //       }
-  //     },
-  //     {
-  //       validateFunction: function (rule, value, data, callback) {
-  //         if (!formData.value?.payFrom || (Number(value) > formData.value?.payFrom ? Number(formData.value?.payFrom) : 0)) {
-  //           return true
-  //         } else {
-  //           callback('应高于最低薪资')
-  //         }
-  //       }
-  //     }
-  //   ]
-	// },
-	// payUnit: salary.value?.length ? { rules: null } : {
-	// 	rules: [{required: true, errorMessage: '请选择计薪时段' }]
-	// },
+	payFrom:{
+		rules: [
+      {
+        required: true,
+        errorMessage: '请填写最低薪资',
+      },
+      {
+        validateFunction: function (rule, value, data, callback) {
+          if (value < 1) {
+            callback('数额不得小于1')
+          }
+          return true
+        }
+      },
+      {
+        validateFunction: function (rule, value, data, callback) {
+          if (!formData.value?.payTo || (Number(value) < formData.value.payTo ? Number(formData.value.payTo) : 0)) {
+            return true
+          } else {
+            callback('应低于最高薪资')
+          }
+        }
+      }
+    ]
+	},
+	payTo:{
+		rules: [
+      {
+        required: true,
+        errorMessage: '请填写最高薪资',
+      },
+      {
+        validateFunction: function (rule, value, data, callback) {
+          if (value < 1) {
+            callback('数额不得小于1')
+          }
+          return true
+        }
+      },
+      {
+        validateFunction: function (rule, value, data, callback) {
+          if (!formData.value?.payFrom || (Number(value) > formData.value?.payFrom ? Number(formData.value?.payFrom) : 0)) {
+            return true
+          } else {
+            callback('应高于最低薪资')
+          }
+        }
+      }
+    ]
+	},
+	payUnit: {
+		rules: [{required: true, errorMessage: '请选择计薪时段' }]
+	},
 	areaId:{
 		rules: [{required: true, errorMessage: '请选择工作城市' }]
 	},
@@ -121,14 +149,6 @@ const rules = {
 	},
 }
 
-// 薪资面议
-const salary = ref(false)
-const salaryChange = (e) => {
-  // const value = e.detail.value.length ? e.detail.value[0] : ''
-  // if (value) {
-  // }
-}
-
 const jobType = ref([])
 const getJobTypeList = async () => {
   const res = await getEnterprisePubJobTypePermission()
@@ -136,14 +156,56 @@ const getJobTypeList = async () => {
 }
 getJobTypeList()
 
-const payToMin = ref(1)
+// 薪资面议
+const salaryStep = 1000
+const payToMin = ref(0)
 const payChange = (val) => {
   payToMin.value = val
-  if (val > formData.value.payTo) formData.value.payTo = val
+  if (val > formData.value.payTo) formData.value.payTo = val + salaryStep
+}
+
+const portraitRef = ref()
+const portraitOpen = () => {
+  portraitRef.value?.handleOpen()
+}
+const portraitSubmit = (arr) => {
+  tagList.value = arr?.length ? arr : []
+}
+
+const typeChange = (e) => {
+  const bool = Boolean(e?.detail?.value?.length ? e.detail.value[0]?.text === '实习' : false)
+  emit('requireTypeChange', bool)
+}
+
+const form = ref()
+const salary = ref(props.data?.payUnit === null ? [1] : [])
+const getQuery = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+  const obj = {
+    hirePrice: 0,
+    hire: false,
+    salary: Boolean(salary.value?.length),
+    tagList: tagList.value,
+    ...formData.value
+  }
+  
+  obj && Object.keys(obj).length && Object.keys(obj).forEach(key => {
+    if (['areaId', 'eduType', 'expType'].includes(key) && obj[key] === -1) obj[key] = null 
+    if (obj.salary && ['payFrom', 'payTo', 'payUnit'].includes(key)) obj[key] = null 
+  })
+
+  console.log('岗位要求:', obj)
+  return obj
 }
 
+defineExpose({
+  getQuery
+})
+
 </script>
 <style lang="scss" scoped>
+$px: 30rpx;
 .positionTemplate {
   display: flex;
  .picker {
@@ -155,9 +217,22 @@ const payChange = (val) => {
   line-height: 34px;
  }
 }
-
-// :deep(.uni-forms-item__content) {
-// 	max-width: 100%;
-// 	overflow: hidden;
-// }
+.tagsBox {
+  display: flex;
+  flex-wrap: wrap;
+  .tags {
+    padding-top: 10px;
+    display: flex;
+    flex-wrap: wrap;
+    .tag {
+      margin: 0 10rpx 10rpx 0;
+      border: 2rpx solid #00B760;
+      color: #00B760;
+      white-space: nowrap;
+      padding: 4rpx 10rpx;
+      border-radius: 10rpx;
+      font-size: 24rpx;
+    }
+  }
+}
 </style>

+ 163 - 7
components/positionAdd/index.vue

@@ -1,24 +1,174 @@
 <template>
   <view class="ss-m-x-20 ss-p-b-100">
-		<uni-section class="ss-m-y-20" title="职位基本信息" >
+    <uni-section v-if="show" class="ss-m-y-20" title="职位基本信息">
       <template v-slot:decoration>
         <view class="decoration decoration1">1</view>
       </template>
-      <BaseInfo></BaseInfo>
+      <!-- 基本信息 -->
+      <baseInfo ref="baseInfoRef" :data="itemData"></baseInfo>
     </uni-section>
-    <uni-section class="ss-m-y-20" title="岗位要求" >
+    <uni-section v-if="show" class="ss-m-y-20" title="岗位要求">
       <template v-slot:decoration>
         <view class="decoration decoration2">2</view>
       </template>
-      <requirement></requirement>
+      <!-- 岗位要求 -->
+      <requirement ref="requirementRef" :data="itemData" @requireTypeChange="bool => isStudent = bool"></requirement>
     </uni-section>
+    <template v-if="isStudent && showExtend">
+      <uni-section class="ss-m-y-20" title="其他">
+        <template v-slot:decoration>
+          <view class="decoration decoration3">3</view>
+        </template>
+        <!-- 扩展信息 -->
+        <extendInfo ref="extendRef" :data="extendData"></extendInfo>
+      </uni-section>
+    </template>
+    <view class="f-horizon-center">
+      <button type="primary" size="default" class="send-button" @click="getSubmitParams">提 交</button>
+    </view>
+    <!-- 支付 -->
+    <payPopup v-if="showPay" ref="payRef" :amount="amount" @paySuccess="paySuccess" @close="payClose"></payPopup>
   </view>
 </template>
+
 <script setup>
-import BaseInfo  from './components/baseInfo.vue'
-import requirement  from './components/requirement.vue'
+import baseInfo from './components/baseInfo.vue'
+import requirement from './components/requirement.vue'
+import extendInfo from './components/extend.vue'
+import { ref, nextTick } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import payPopup from '@/components/payPopup'
+import { dealDictObjData } from '@/utils/position'
+import {
+  saveJobAdvertised,
+  saveJobAdvertisedExtend,
+  getJobDetails,
+  getJobAdvertisedExtend,
+} from '@/api/new/position'
+
+// 添加只为之后是否需要额外操作
+// const props = defineProps({
+// })
+
+let jobId = ''
+let fairId = ''
+onLoad((options) => {
+  jobId = options?.jobId || props.jobId || ''
+  fairId = options?.fairId || ''
+  console.log('jobId:', jobId); console.log('fairId:', fairId)
+  if (jobId) getPositionDetail(jobId)
+})
+
+// 获取编辑的职位详情
+const show = ref(false)
+const itemData = ref({})
+const getPositionDetail = async (id) => {
+  const res = await getJobDetails({ id })
+  if (!res?.data && !Object.keys(res.data).length) return
+  itemData.value = {...res.data, ...dealDictObjData({}, res.data)}
+  show.value = true
+  console.log('itemData:', itemData.value)
+  if (itemData.value?.type === '3') {
+    isStudent.value = true
+    getPositionExtendDetail(id)
+  }
+}
+getPositionDetail('1904144364792340482')
+
+// 获取编辑的职位详情
+const showExtend = ref(false)
+const extendData = ref({})
+const getPositionExtendDetail = async (jobId) => {
+  const res = await getJobAdvertisedExtend(jobId)
+  extendData.value = res?.data || {}
+  showExtend.value = true
+  console.log('extendData:', extendData.value)
+}
+
+const baseInfoRef = ref(null)
+const requirementRef = ref(null)
+const extendRef = ref(null)
+let submitParams = null
+const getSubmitParams = async() => {
+  const baseInfo = await baseInfoRef.value.getQuery()
+  const requirement = await requirementRef.value.getQuery()
+  if (!baseInfo || !requirement) return
+  
+  submitParams = {
+    ...baseInfo,
+    ...requirement,
+    fair: Boolean(fairId), // fair:是否为招聘会职位编辑-必填
+    currency_type: 0, // currency_type: 写死0(人民币)
+    source: fairId ? '2' : '0', // source: 0职位管理|1招聘会
+    bizId:  fairId ? fairId : null,
+  }
+  if (jobId) submitParams.id = jobId  // 有id则为编辑
+  saveEmit()
+}
+// 
+const price = 1 // 39900
+const isStudent = ref(false)
+let _jobId = ''
+const saveEmit = async (retry) => {
+  try {
+    uni.showLoading({ title: '操作中...', mask: true })
+    const res = await saveJobAdvertised(submitParams)
+    _jobId = res?.data || res || ''
+    if (isStudent.value) handleSaveExtend() // 保存扩展信息
+    // status:99为待支付职位,弹窗支付
+    if (submitParams?.status && submitParams?.status === '99') {
+      showPay.value = true
+      console.log('1payRef:', payRef.value)
+      uni.showToast({ title: '当前可发布职位额度不足,请支付', icon: 'none', duration: 2000 })
+      nextTick(() => {
+        // 金额*100,页面展示/100
+        console.log('2payRef:', payRef.value?.handleOpen)
+        payRef.value && payRef.value.handleOpen({ spuId: _jobId||'', spuName: submitParams?.name||'', price, type: 1 })
+      })
+      return
+    }
+    uni.switchTab({ url: '/pages/index/position' })
+    setTimeout(() => { uni.showToast({ title: '发布成功', icon: 'success' }) }, 1000)
+
+  } catch (error) {
+    console.log('error:', error)
+    // 可发布职位额度不足时,将status设为99重新提交
+    if (error?.msg === '企业额度已超过') {
+      showPay.value = true
+      submitParams.status = '99'
+      setTimeout(() => {
+        if (!retry) saveEmit(true) // true:重新提交避免死循环
+      }, 1000)
+    }
+  } finally {
+    uni.hideLoading()
+  }
+}
+
+const payRef = ref(null)
+const showPay = ref(false)
+const paySuccess = () => {
+  uni.switchTab({ url: '/pages/index/position' })
+  setTimeout(() => { uni.showToast({ title: '发布成功', icon: 'success' }) }, 1000)
+}
+const payClose = () => {
+  uni.switchTab({ url: '/pages/index/position' }) // 不支持传参
+}
+
+// 保存扩展信息
+const handleSaveExtend = async () => {
+  if (!_jobId) return
+  try {
+    const extend = await extendRef.value && extendRef.value.getQuery() || null
+    if (!extend) return
+    await saveJobAdvertisedExtend({ ...extend, jobId: _jobId })
+  } catch (error) {
+    console.error('保存扩展信息失败', error)
+  }
+}
 
 </script>
+
 <style scoped lang="scss">
 .decoration {
   color: #fff;
@@ -30,10 +180,16 @@ import requirement  from './components/requirement.vue'
   text-align: center;
   margin-right: 4px;
 }
+
 .decoration1 {
-  background-color: #00B760;
+  background-color: #00b760;
 }
+
 .decoration2 {
   background-color: #7a87c9;
 }
+
+.decoration3 {
+  background-color: #1caaf2;
+}
 </style>

+ 8 - 4
pages/index/position.vue

@@ -52,11 +52,17 @@ import layoutPage from '@/layout'
 import PositionList from '@/components/PositionList'
 import { dealDictArrayData } from '@/utils/position'
 import { getJobAdvertisedList } from '@/api/new/position'
-import { onShow } from '@dcloudio/uni-app'
+import { onShow, onLoad } from '@dcloudio/uni-app'
 import { getAccessToken } from '@/utils/request'
 import { showAuthModal } from '@/hooks/useModal'
 
-const tab = ref(2)
+const tab = ref(1)
+
+onLoad((options) => {
+  console.log('onLoad:', options)
+  if (options?.tab) tab.value = Number(options?.tab)
+})
+
 const tabList = [
   { label: '待发布', value: 0, status: 99 },
   { label: '招聘中', value: 1, status: 0 },
@@ -114,7 +120,6 @@ const getData = async () => {
 
 // 设置自定义tabbar选中值
 onShow(() => {
-  console.log('onShow:', 789)
   const currentPage = getCurrentPages()[0] // 获取当前页面实例
   const currentTabBar = currentPage?.getTabBar?.()
 
@@ -124,7 +129,6 @@ onShow(() => {
 })
 
 const init = () => {
-  console.log('123456:', 'init')
   query.value.pageNo = 1
   getData()
 }

+ 11 - 0
utils/date.js

@@ -38,6 +38,17 @@ export const convertYearMonthToTimestamp = (yearMonth) => {
   return timestamp  
 }
 
+// 法将 YYYY-MM-DD、YYYY-MM 或 YYYY 格式的日期转换为时间戳
+export const dateToTimestamp = (dateStr) => {  
+  if (!dateStr) return null
+  if (dateStr.length === 4) {
+      dateStr += '-01-01' // YYYY -> YYYY-01-01
+  } else if (dateStr.length === 7) {
+      dateStr += '-01' // YYYY-MM -> YYYY-MM-01
+  }
+  return new Date(dateStr).getTime()
+}
+
 export const getTimeDifferenceInChinese = (startTime, endTime) => {
   // 将时间戳转换为 Date 对象(假设时间戳单位为毫秒)
   const startDate = startTime ? new Date(startTime) : new Date();