|
@@ -0,0 +1,370 @@
|
|
|
+<template>
|
|
|
+ <div class="top">
|
|
|
+ <div class="d-flex" v-if="positionCategory.length">
|
|
|
+ <div class="font-weight-bold position-category-left">职位类别:</div>
|
|
|
+ <div class="position-category-right">
|
|
|
+ <span
|
|
|
+ :class="['category-item', {'default-active': k.active}, {'font-weight-bold': k.active}]"
|
|
|
+ v-for="k in positionCategory"
|
|
|
+ :key="k.id"
|
|
|
+ @click.stop="handleClickCategory(k)"
|
|
|
+ >{{ k.id === -1 ? `${k.label}` : `${k.label} (${k.number})` }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="d-flex mt-1 justify-space-between">
|
|
|
+ <conditionFilter v-if="show" ref="conditionFilterRef" :showFilterList="showFilterList" @reset="handleReset" @change="handleQueryChange"></conditionFilter>
|
|
|
+ <div style="width: 220px;" class="mt-2">
|
|
|
+ <v-text-field
|
|
|
+ v-model="query.content"
|
|
|
+ variant="outlined"
|
|
|
+ label="查找职位关键字"
|
|
|
+ hide-details
|
|
|
+ color="primary"
|
|
|
+ append-inner-icon="mdi-magnify"
|
|
|
+ @click:append-inner="handleSearch('content', { values: query.content })"
|
|
|
+ @keyup.enter="handleSearch('content', { values: query.content })"
|
|
|
+ >
|
|
|
+ </v-text-field>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <v-divider class="mt-5"></v-divider>
|
|
|
+ <div class="bottom mt-4">
|
|
|
+ <div v-if="list.length">
|
|
|
+ <div
|
|
|
+ v-for="(val, i) in list"
|
|
|
+ :key="i"
|
|
|
+ :class="['bottom-item', {'border-bottom-dashed': i !== list.length -1}, 'd-flex', 'justify-space-between', 'cursor-pointer']"
|
|
|
+ @mouseenter="val.active = true"
|
|
|
+ @mouseleave="val.active = false"
|
|
|
+ >
|
|
|
+ <div>
|
|
|
+ <p v-if="val.job.name.includes('style')" :class="['name', {'default-active': val.active }]" v-html="val.job.name" @click.stop="handlePosition(val)"></p>
|
|
|
+ <p v-else :class="['name', {'default-active': val.active }]" @click.stop="handlePosition(val)">{{ formatName(val.job.name) }}</p>
|
|
|
+ <div style="line-height: 40px;">
|
|
|
+ <span v-for="k in desc" :key="k.mdi">
|
|
|
+ <span v-if="val.job[k.value] || k.value === 'areaName'" class="mr-5">
|
|
|
+ <v-icon color="var(--color-666)" size="15">{{ k.mdi }}</v-icon>
|
|
|
+ <span class="ml-1 tag-text">
|
|
|
+ {{ k.value === 'areaName' ? !val.job.areaId ? '全国' : val.job.area?.str : val.job[k.value] }}
|
|
|
+ <!-- {{ (k.value === 'areaName' && !val.job.areaId) ? '全国' : val.job[k.value] }} -->
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="!val.active" class="text-right">
|
|
|
+ <p v-if="!val.job.payFrom && !val.job.payTo" class="salary">面议</p>
|
|
|
+ <p v-else class="salary">{{ val.job.payFrom ? val.job.payFrom + '-' : '' }}{{ val.job.payTo }}{{ val.job.payName ? '/' + val.job.payName : '' }}</p>
|
|
|
+ <div class="update-time">{{ timesTampChange(val.job.updateTime) }} 刷新</div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="account-info">
|
|
|
+ <v-avatar :image="getUserAvatar(val.contact.avatar, val.contact.sex)"></v-avatar>
|
|
|
+ <span class="account-label">{{ val.contact.name }}{{ val.contact.postNameCn ? ' · ' + val.contact.postNameCn : '' }}</span>
|
|
|
+ <span>
|
|
|
+ <v-btn class="half-button" color="primary" size="small" @click="toDetails(val)">立即沟通</v-btn>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <MPagination
|
|
|
+ :total="total"
|
|
|
+ :page="pageInfo.pageNo"
|
|
|
+ :limit="pageInfo.pageSize"
|
|
|
+ @handleChange="handleChangePage"
|
|
|
+ ></MPagination>
|
|
|
+ </div>
|
|
|
+ <Empty v-else :elevation="false"></Empty>
|
|
|
+
|
|
|
+ <!-- 快速登录 -->
|
|
|
+ <loginPage v-if="showLogin" @loginSuccess="loginSuccess" @close="loginClose"></loginPage>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+defineOptions({ name: 'recruitment-positions'})
|
|
|
+import { reactive, ref } from 'vue'
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
+import { timesTampChange } from '@/utils/date'
|
|
|
+import { getDict } from '@/hooks/web/useDictionaries'
|
|
|
+import { dealDictObjData } from '@/utils/position'
|
|
|
+import { prologue, defaultText } from '@/hooks/web/useIM'
|
|
|
+import { getUserAvatar } from '@/utils/avatar'
|
|
|
+import { getJobAdvertisedPositionCount, getJobAreaByEnterpriseId, getJobAdvertisedSearch } from '@/api/position'
|
|
|
+import MPagination from '@/components/CtPagination'
|
|
|
+import conditionFilter from '@/views/recruit/personal/position/components/conditionFilter'
|
|
|
+import loginPage from '@/views/common/loginDialog.vue'
|
|
|
+import { getToken } from '@/utils/auth'
|
|
|
+import Snackbar from '@/plugins/snackbar'
|
|
|
+import { checkPersonBaseInfo } from '@/utils/check'
|
|
|
+import dialogExtend from '@/plugins/dialogExtend'
|
|
|
+import { formatName } from '@/utils/getText'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ info: {
|
|
|
+ type: Object,
|
|
|
+ default: () => {}
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const total = ref(0)
|
|
|
+const pageInfo = ref({
|
|
|
+ pageSize: 10,
|
|
|
+ pageNo: 1
|
|
|
+})
|
|
|
+let query = reactive({})
|
|
|
+const route = useRoute(); const router = useRouter()
|
|
|
+let routeQuery = (route?.query && route.query && Object.keys(route?.query).length) ? route.query : null
|
|
|
+if (routeQuery?.content) query.content = routeQuery?.content || ''
|
|
|
+if (routeQuery) query = routeQuery
|
|
|
+
|
|
|
+// 职位详情
|
|
|
+const handlePosition = (val) => {
|
|
|
+ window.open(`/recruit/personal/position/details/${val.job.id}`)
|
|
|
+}
|
|
|
+
|
|
|
+// 职位类型
|
|
|
+const positionList = ref([])
|
|
|
+const getDictData = async () => {
|
|
|
+ const { data } = await getDict('positionData', {}, 'positionData')
|
|
|
+ positionList.value = data
|
|
|
+}
|
|
|
+getDictData()
|
|
|
+
|
|
|
+const show = ref(false)
|
|
|
+const showFilterList = ref([
|
|
|
+ { key: 'expType', isSingle: true },
|
|
|
+ { key: 'eduType', isSingle: true },
|
|
|
+ { key: 'payScope', isSingle: true },
|
|
|
+])
|
|
|
+const getProvideData = (list) => {
|
|
|
+ if (!list?.length) return show.value = true
|
|
|
+ getDict('menduner_area_type', {}, 'areaList').then(({ data }) => {
|
|
|
+ data = data?.length && data || []
|
|
|
+ const arr = list.map(e => {
|
|
|
+ const obj = data.find(k => k.id === Number(e.key))
|
|
|
+ if (!obj) return
|
|
|
+ return { label: obj.name, value: obj.id }
|
|
|
+ }).filter(Boolean)
|
|
|
+ if (arr?.length) showFilterList.value.unshift({ key: 'areaIds', isSingle: true, provideData: arr})
|
|
|
+ show.value = true
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 职位类别&工作地点
|
|
|
+const positionCategory = ref([])
|
|
|
+const getData = async () => {
|
|
|
+ const data = await getJobAdvertisedPositionCount({ enterpriseId: props.info.enterprise.id })
|
|
|
+ const areaList = await getJobAreaByEnterpriseId({ enterpriseId: props.info.enterprise.id })
|
|
|
+ getProvideData(areaList)
|
|
|
+ const list = data.map(val => {
|
|
|
+ const value = positionList.value.find(e => Number(e.id) === Number(val.key))
|
|
|
+ if (!value) return
|
|
|
+ return { id: value.id, label: value.nameCn, number: val.value, active: false }
|
|
|
+ }).filter(Boolean)
|
|
|
+ positionCategory.value = [{ id: -1, label: '全部', active: true }, ...list]
|
|
|
+}
|
|
|
+const getPoAr = async () => {
|
|
|
+ await getData()
|
|
|
+ // 职位类别回显
|
|
|
+ if (routeQuery?.positionId) {
|
|
|
+ positionCategory.value.map(e => e.active = false)
|
|
|
+ positionCategory.value.find(e => e.id === Number(routeQuery.positionId)).active = true
|
|
|
+ }
|
|
|
+}
|
|
|
+getPoAr()
|
|
|
+
|
|
|
+// 职位类别选中
|
|
|
+const handleClickCategory = (k) => {
|
|
|
+ positionCategory.value.map(e => e.active = false)
|
|
|
+ k.active = !k.active
|
|
|
+ handleSearch('positionId', { values: [k.id] })
|
|
|
+}
|
|
|
+
|
|
|
+const dealRouteQuery = () => {
|
|
|
+ const arr = Object.keys(query).map(e => {
|
|
|
+ if (Array.isArray(query[e]) && !query[e].length) {
|
|
|
+ delete query[e]
|
|
|
+ }
|
|
|
+ if (!query[e]) delete query[e]
|
|
|
+ if (e !== 'pageSize' && e !== 'pageNo' && e !== 'enterpriseId') return `${e}=${query[e]}`
|
|
|
+ }).filter(Boolean)
|
|
|
+ const str = ['key=recruitmentPositions', ...arr].join('&')
|
|
|
+ if (str) router.replace(`${route.path}?${str}`)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = (key, { values = [] }) => {
|
|
|
+ if (key) {
|
|
|
+ if (values === -1 || !values || values[0] === -1 || !values.length) delete query[key]
|
|
|
+ else if (['payScope', 'positionId'].includes(key)) query[key] = values?.length ? values[values.length-1] : '' // 单选且传递字符串
|
|
|
+ else query[key] = values
|
|
|
+ }
|
|
|
+ dealRouteQuery()
|
|
|
+ getPositionList(true)
|
|
|
+}
|
|
|
+
|
|
|
+// 职位列表
|
|
|
+const list = ref([])
|
|
|
+// 职位列表
|
|
|
+const getPositionList = async (isSearch) => {
|
|
|
+ const queryParams = {
|
|
|
+ ...query,
|
|
|
+ ...pageInfo.value,
|
|
|
+ enterpriseId: props.info.enterprise.id
|
|
|
+ }
|
|
|
+ delete queryParams.key
|
|
|
+ for (const key in queryParams) {
|
|
|
+ if (['expType', 'eduType'].includes(key) && queryParams[key] === '9999') delete queryParams[key]
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isSearch) query.pageNo = 1
|
|
|
+ const { list: arr, total: number } = await getJobAdvertisedSearch(queryParams)
|
|
|
+ total.value = number
|
|
|
+ list.value = arr.map(e => {
|
|
|
+ e.active = false
|
|
|
+ e.job = { ...e.job, ...dealDictObjData({}, e.job) }
|
|
|
+ return e
|
|
|
+ })
|
|
|
+}
|
|
|
+getPositionList()
|
|
|
+
|
|
|
+const handleChangePage = (index) => {
|
|
|
+ pageInfo.value.pageNo = index
|
|
|
+ getPositionList()
|
|
|
+}
|
|
|
+
|
|
|
+// 参数改变
|
|
|
+const handleQueryChange = (key, val) => { // val为字符串,数组的话用_下划线分隔
|
|
|
+ pageInfo.value.pageNo = 1
|
|
|
+ const values = val ? val.split('_') : []
|
|
|
+ handleSearch(key, { values })
|
|
|
+}
|
|
|
+
|
|
|
+// 清空筛选条件
|
|
|
+const handleReset = async () => {
|
|
|
+ pageInfo.value.pageNo = 1
|
|
|
+ showFilterList.value.forEach(e => {
|
|
|
+ delete query[e.key]
|
|
|
+ })
|
|
|
+ handleSearch(null, {})
|
|
|
+}
|
|
|
+
|
|
|
+// 城市、学历、工作经验
|
|
|
+const desc = [
|
|
|
+ { mdi: 'mdi-map-marker-outline', value: 'areaName' },
|
|
|
+ { mdi: 'mdi-school-outline', value: 'eduName' },
|
|
|
+ { mdi: 'mdi-clock-time-ten-outline', value: 'expName' }
|
|
|
+]
|
|
|
+
|
|
|
+let toDetailsInfo = {}
|
|
|
+// 沟通
|
|
|
+const toDetails = async (info) => {
|
|
|
+ if (info) toDetailsInfo = info // 快速登录弹窗回调使用
|
|
|
+ else info = toDetailsInfo
|
|
|
+ if (!getToken()) {
|
|
|
+ showLogin.value = true // 打开快速登录弹窗
|
|
|
+ Snackbar.warning('您还未登录,请先登录后再试')
|
|
|
+ //
|
|
|
+ loginCloseWarningWord = '您已取消登录,无法对职位进行沟通' // 取消登录提示语
|
|
|
+ nextFunc.value = toDetails // 登录成功后要执行的操作 (toDetails执行不成功,原因未找到)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!checkPersonBaseInfo()) { // 强制填写个人信息
|
|
|
+ dialogExtend('necessaryInfoDialog').then(() => {
|
|
|
+ toDetails(toDetailsInfo)
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const userId = info.contact.userId
|
|
|
+ const enterpriseId = info.contact.enterpriseId
|
|
|
+ const textObj = {
|
|
|
+ text: defaultText,
|
|
|
+ positionInfo: { ...info.job, enterprise: info.enterprise, contact: info.contact },
|
|
|
+ }
|
|
|
+ await prologue({userId, enterpriseId, text: JSON.stringify(textObj)})
|
|
|
+ let url = `/recruit/personal/message?id=${info.job.id}`
|
|
|
+ if (info.contact.enterpriseId) {
|
|
|
+ url += `&enterprise=${info.contact.enterpriseId}`
|
|
|
+ }
|
|
|
+
|
|
|
+ window.open(url)
|
|
|
+}
|
|
|
+
|
|
|
+const showLogin = ref(false)
|
|
|
+const nextFunc = ref(null)
|
|
|
+let loginCloseWarningWord = ''
|
|
|
+// 快速登录
|
|
|
+const loginSuccess = () => {
|
|
|
+ showLogin.value = false
|
|
|
+ Snackbar.success('登录成功')
|
|
|
+ if (nextFunc.value) nextFunc.value()
|
|
|
+}
|
|
|
+
|
|
|
+const loginClose = () => {
|
|
|
+ showLogin.value = false
|
|
|
+ Snackbar.warning(loginCloseWarningWord)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.bottom-item {
|
|
|
+ width: 100%;
|
|
|
+ height: 68px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.name {
|
|
|
+ position: relative;
|
|
|
+ max-width: 30vw;
|
|
|
+ margin-right: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.salary {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--v-error-base);
|
|
|
+ line-height: 22px;
|
|
|
+ flex: none;
|
|
|
+}
|
|
|
+.tag-text {
|
|
|
+ color: var(--color-222);
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.update-time {
|
|
|
+ color: var(--color-666);
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 40px;
|
|
|
+}
|
|
|
+.account-info {
|
|
|
+ line-height: 52px;
|
|
|
+ .account-label {
|
|
|
+ color: var(--color-666);
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin: 0 10px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.position-category-left {
|
|
|
+ width: 80px;
|
|
|
+}
|
|
|
+.position-category-right {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.category-item {
|
|
|
+ display: inline-block;
|
|
|
+ margin-right: 20px;
|
|
|
+ font-size: 15px;
|
|
|
+ color: var(--color-666);
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ color: var(--v-primary-base);
|
|
|
+ }
|
|
|
+}
|
|
|
+:deep(.v-field__input) {
|
|
|
+ height: 28px;
|
|
|
+ padding: 0 0 0 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ min-height: 28px;
|
|
|
+}
|
|
|
+</style>
|