意向字段全部改成使用招聘分类

This commit is contained in:
xuxin
2026-06-05 17:27:07 +08:00
parent 8d6282c3e6
commit c17f75c707
10 changed files with 85 additions and 95 deletions
+4 -7
View File
@@ -75,8 +75,6 @@ export interface JobListParams {
categoryIds?: number[]
/** 行业 ID 列表 */
industryIds?: number[]
/** 工作类型:0=全职 1=兼职 */
employmentType?: number
/** 指定岗位 ID 列表(用于收藏列表) */
jobIds?: number[]
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
@@ -100,8 +98,10 @@ export interface JobIntention {
regionCodes?: string[]
/** 期望行业 ID 列表 */
industryIds?: number[]
/** 就业类型:0=全职1=实习 */
employmentType?: number
/** 就业类型:0=校招1=实习2=社招 */
employmentType?: number | null
/** 招聘分类:0=社招,1=校招,2=实习,3=其他 */
recruitCategory?: number | null
}
/**
@@ -128,9 +128,6 @@ export function saveJobIntention(data: JobIntention) {
* @param params 岗位列表查询参数
*/
export function fetchJobList(params: JobListParams = {}) {
//兼容旧的数据组件,让招聘分类参数从工作类型那里拿,工作类型不需要传了
params.recruitCategory = params.employmentType
delete params.employmentType
return request.post<any, ApiResult<JobPageData>>('/job/list', {
pageNum: params.pageNum ?? 1,
pageSize: params.pageSize ?? 15,
+11 -1
View File
@@ -346,11 +346,21 @@
font-weight: 600;
font-size: 0.16rem;
color: #FFFFFF;
cursor: default;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.08rem;
transition: opacity 0.2s;
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
&:not(:disabled):hover {
opacity: 0.9;
}
}
/* 登录中转圈动画 */
+2 -5
View File
@@ -109,6 +109,7 @@
import { ref, computed, onMounted, watch } from 'vue'
import { fetchJobDetail } from '@/api/jobs'
import type { JobDetailData } from '@/api/jobs'
import { formatEmploymentType } from '@/stores/index'
/** 组件 Props */
const props = defineProps<{
@@ -150,11 +151,7 @@ const formatTime = computed(() => {
return ''
})
/** 工作类型映射 */
function formatEmploymentType(type: number | undefined): string {
const map: Record<number, string> = { 0: '全职', 1: '兼职' }
return map[type ?? -1] ?? ''
}
/** 工作类型映射 — 使用全局统一的 formatEmploymentType */
/** 加载岗位详情 */
async function loadDetail() {
+4 -4
View File
@@ -66,7 +66,7 @@
<span v-for="name in intentionCategoryNames" :key="'cat-' + name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span v-for="name in intentionIndustryNames" :key="'ind-' + name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span v-for="name in intentionRegionNames" :key="'reg-' + name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span class="agent-setting-panel__goal-tag">{{ intentionEmploymentLabel }}</span>
<span v-if="intentionEmploymentLabel" class="agent-setting-panel__goal-tag">{{ intentionEmploymentLabel }}</span>
<!-- 无意向时的空状态 -->
</div>
@@ -282,8 +282,8 @@ const intentionIndustryNames = computed(() => (store.state.jobIntention?.industr
/** 求职意向 — 地区名称列表 */
const intentionRegionNames = computed(() => (store.state.jobIntention?.regionCodes || []).map((code: string) => resolveRegionName(code)).filter(Boolean))
/** 求职意向 — 就业类型文案 */
const intentionEmploymentLabel = computed(() => formatEmploymentType(store.state.jobIntention?.employmentType))
/** 求职意向 — 招聘分类文案 */
const intentionEmploymentLabel = computed(() => formatEmploymentType(store.state.jobIntention?.recruitCategory))
// ==================== 浏览器插件 ====================
@@ -336,7 +336,7 @@ function downloadExtension() {
// ==================== 求职助手配置 ====================
/** 是否为实习类型 */
const isInternship = computed(() => store.state.jobIntention?.employmentType === 1)
const isInternship = computed(() => store.state.jobIntention?.recruitCategory === 2)
/** 配置表单数据 */
const configForm = ref({
+4 -4
View File
@@ -403,7 +403,7 @@ const showJobGoalDialog = ref(false)
const intentionCategoryNames = computed(() => (store.state.jobIntention.categoryIds || []).map((id: number) => resolveJobCategoryName(id)))
const intentionIndustryNames = computed(() => (store.state.jobIntention.industryIds || []).map((id: number) => resolveIndustryName(id)))
const intentionRegionNames = computed(() => (store.state.jobIntention.regionCodes || []).map((code: string) => resolveRegionName(code)))
const intentionEmploymentLabel = computed(() => formatEmploymentType(store.state.jobIntention.employmentType))
const intentionEmploymentLabel = computed(() => formatEmploymentType(store.state.jobIntention.recruitCategory))
interface MatchedJobItem extends JobListItem { feedback: string }
const matchedJobs = ref<MatchedJobItem[]>([])
@@ -418,7 +418,7 @@ async function loadMatchedJobs() {
loadingMatchJobs.value = true; matchedJobs.value = []
try {
const intention = store.state.jobIntention
const res = await fetchJobList({ pageNum: 1, pageSize: 30, regionCodes: intention.regionCodes?.length ? intention.regionCodes : undefined, categoryIds: intention.categoryIds?.length ? intention.categoryIds : undefined, industryIds: intention.industryIds?.length ? intention.industryIds : undefined, employmentType: intention.employmentType ?? undefined })
const res = await fetchJobList({ pageNum: 1, pageSize: 30, regionCodes: intention.regionCodes?.length ? intention.regionCodes : undefined, categoryIds: intention.categoryIds?.length ? intention.categoryIds : undefined, industryIds: intention.industryIds?.length ? intention.industryIds : undefined, recruitCategory: intention.recruitCategory ?? undefined })
if (res.code === '0' && res.data && res.data.list.length > 0) {
const shuffled = [...res.data.list].sort(() => Math.random() - 0.5)
matchedJobs.value = shuffled.slice(0, 3).map(item => ({ ...item, feedback: '' }))
@@ -434,7 +434,7 @@ function handleDislike(index: number) {
}
// ==================== 第3步:网申常见问题 ====================
const isInternship = computed(() => store.state.jobIntention.employmentType === 1)
const isInternship = computed(() => store.state.jobIntention.recruitCategory === 2)
const step3Sub = ref(1)
const step3Form = reactive({
acceptDeptTransfer: '', acceptLocationTransfer: '',
@@ -481,7 +481,7 @@ const setupComplete = ref(false)
function handleStep4Complete() {
const step4Data = settingsPanelRef.value?.getData()
const allSettings = {
jobType: store.state.jobIntention.employmentType === 1 ? 1 : 2,
jobType: store.state.jobIntention.recruitCategory === 2 ? 1 : 2,
agentMode: step4Data?.agentMode ?? 1,
weeklyTarget: step4Data?.weeklyTarget ?? 2,
autoOptimizeResume: step4Data?.autoOptimizeResume ?? 1,
+6 -6
View File
@@ -62,10 +62,10 @@
/>
</div>
<!-- 工作类型选择模块 -->
<!-- 招聘分类选择模块 -->
<div class="job-goal-dialog__section ">
<div class="job-goal-dialog__label">*工作类型</div>
<!-- 工作类型按钮组 -->
<div class="job-goal-dialog__label">*招聘分类</div>
<!-- 招聘分类按钮组 -->
<div class="job-goal-dialog__type-group">
<button
v-for="t in jobTypes"
@@ -118,7 +118,7 @@ const selectedIndustryIds = ref<number[]>([])
const selectedRegionCodes = ref<string[]>([])
const selectedJobType = ref('全职')
/** 工作类型选项列表 — 从全局常量提取 label 生成 */
/** 招聘分类选项列表 — 从全局常量提取 label 生成 */
const jobTypes = JOB_TYPE_OPTIONS.map(item => item.label)
/** 弹窗打开时从 store 同步数据到本地编辑副本 */
@@ -128,7 +128,7 @@ watch(() => props.modelValue, (v) => {
selectedCategoryIds.value = [...(intention.categoryIds || [])]
selectedIndustryIds.value = [...(intention.industryIds || [])]
selectedRegionCodes.value = [...(intention.regionCodes || [])]
selectedJobType.value = formatEmploymentType(intention.employmentType)
selectedJobType.value = formatEmploymentType(intention.recruitCategory)
}
})
@@ -175,7 +175,7 @@ async function handleSave() {
categoryIds: [...selectedCategoryIds.value],
industryIds: [...selectedIndustryIds.value],
regionCodes: [...selectedRegionCodes.value],
employmentType: JOB_TYPE_OPTIONS.find(o => o.label === selectedJobType.value)?.value ?? 0,
recruitCategory: JOB_TYPE_OPTIONS.find(o => o.label === selectedJobType.value)?.value ?? 0,
})
visible.value = false
} catch (e) {
+3 -3
View File
@@ -130,7 +130,7 @@
<span class="settings-dialog__reminder-tag" v-for="name in intentionRegionNames" :key="name">{{ name }}</span>
</div>
</div>
<div class="settings-dialog__reminder-group">
<div v-if="intentionEmploymentLabel" class="settings-dialog__reminder-group">
<span class="settings-dialog__reminder-group-label">类型</span>
<div class="settings-dialog__reminder-tags">
<span class="settings-dialog__reminder-tag">{{ intentionEmploymentLabel }}</span>
@@ -366,9 +366,9 @@ const intentionRegionNames = computed(() => {
return codes.map((code: string) => resolveRegionName(code))
})
/** 就业类型标签 */
/** 招聘分类标签 */
const intentionEmploymentLabel = computed(() => {
return formatEmploymentType(store.state.jobIntention.employmentType)
return formatEmploymentType(store.state.jobIntention.recruitCategory)
})
/** 编辑目标岗位 — 打开求职目标弹窗 */
+11 -7
View File
@@ -10,21 +10,22 @@ import type { JobIntention } from '@/api/jobs'
import { fetchUserInfo } from '@/api/auth'
import type { UserInfo } from '@/api/auth'
/** 工作类型选项:label → 接口参数 employmentType0=全职 1=实习) */
/** 招聘分类选项:label → 接口参数 recruitCategory */
export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [
{ label: '社招', value: 0 },
{ label: '校招', value: 1 },
{ label: '实习', value: 2 },
]
/** 工作类型映射:数字 → 中文标签 */
/** 招聘分类映射:数字 → 中文标签 */
export const JOB_TYPE_MAP: Record<number, string> = Object.fromEntries(
JOB_TYPE_OPTIONS.map(item => [item.value, item.label]),
)
/** 根据 employmentType 值获取中文标签,未匹配返回"未知" */
/** 根据 recruitCategory 值获取中文标签,未匹配返回空字符串 */
export function formatEmploymentType(type: number | undefined | null): string {
return JOB_TYPE_MAP[type ?? -1] ?? '未知'
if (type === undefined || type === null) return ''
return JOB_TYPE_MAP[type] ?? ''
}
/** 职位列表页缓存数据(从详情页返回时恢复用) */
@@ -135,7 +136,8 @@ export default createStore<RootState>({
categoryIds: [],
regionCodes: [],
industryIds: [],
employmentType: 0,
employmentType: null,
recruitCategory: null,
},
userInfo: null,
showSettings: false,
@@ -189,7 +191,8 @@ export default createStore<RootState>({
categoryIds: data.categoryIds ?? [],
regionCodes: data.regionCodes ?? [],
industryIds: data.industryIds ?? [],
employmentType: data.employmentType ?? 0,
employmentType: data.employmentType ?? null,
recruitCategory: data.recruitCategory ?? null,
}
},
SET_USER_INFO(state, data: UserInfo | null) {
@@ -274,7 +277,8 @@ export default createStore<RootState>({
categoryIds: [],
regionCodes: [],
industryIds: [],
employmentType: 0,
employmentType: null,
recruitCategory: null,
})
},
+11 -11
View File
@@ -447,18 +447,18 @@ const filters = ref<FilterItem[]>([
{ label: '城市', key: 'city', selected: '' },
{ label: '岗位', key: 'position', selected: '' },
{ label: '行业', key: 'industry', selected: '' },
{ label: '工作类型', key: 'jobType', selected: '' },
{ label: '招聘分类', key: 'jobType', selected: '' },
])
/** 工作类型选项映射:从全局 store 常量统一引入 */
/** 招聘分类选项映射:从全局 store 常量统一引入 */
const jobTypeOptions = JOB_TYPE_OPTIONS
/** 工作类型下拉菜单是否显示 */
/** 招聘分类下拉菜单是否显示 */
const showJobTypeDropdown = ref(false)
/** 当前选中的工作类型 — 直接读 store.jobIntention.employmentType */
/** 当前选中的招聘分类 — 直接读 store.jobIntention.recruitCategory */
const selectedEmploymentType = computed<number | null>(
() => store.state.jobIntention.employmentType ?? null,
() => store.state.jobIntention.recruitCategory ?? null,
)
/** 选中的行业 id 数组 — 直接读 store.jobIntention.industryIds */
@@ -500,24 +500,24 @@ function onRegionChange(codes: string[]) {
})
}
/** 点击筛选条件按钮 — 仅工作类型展开下拉 */
/** 点击筛选条件按钮 — 仅招聘分类展开下拉 */
function handleFilterClick(filter: FilterItem) {
if (filter.key === 'jobType') {
showJobTypeDropdown.value = !showJobTypeDropdown.value
}
}
/** 选中工作类型选项 */
/** 选中招聘分类选项 */
function selectJobType(filter: FilterItem, option: { label: string; value: number }) {
filter.selected = option.label
showJobTypeDropdown.value = false
store.dispatch('saveJobIntention', {
...store.state.jobIntention,
employmentType: option.value,
recruitCategory: option.value,
})
}
/** 监听 store 中 employmentType 变化,同步工作类型筛选按钮的显示文字 */
/** 监听 store 中 recruitCategory 变化,同步招聘分类筛选按钮的显示文字 */
watch(selectedEmploymentType, (val) => {
const jobTypeFilter = filters.value.find(f => f.key === 'jobType')
if (jobTypeFilter && val !== null) {
@@ -765,9 +765,9 @@ function buildParams(): JobListParams {
if (selectedIndustryIds.value.length) {
params.industryIds = selectedIndustryIds.value
}
// 工作类型筛选
// 招聘分类筛选
if (selectedEmploymentType.value !== null) {
params.employmentType = selectedEmploymentType.value
params.recruitCategory = selectedEmploymentType.value
}
// 搜索关键词
if (keyword.value.trim()) {
+29 -47
View File
@@ -109,17 +109,25 @@
<span v-else-if="idx === otpValue.length && !isLoggingIn" class="login-view__otp-cursor"></span>
</div>
</div>
<!-- 状态按钮非触发登录 -->
<button class="login-view__status-btn" disabled>
<!-- 状态按钮 三种状态登录中 / 倒计时 / 再次发送 -->
<button
class="login-view__status-btn"
:disabled="isLoggingIn || countdown > 0"
@click="handleResendCode"
>
<!-- 登录中状态 -->
<template v-if="isLoggingIn">
<span class="login-view__spinner"></span>
<span>登录中</span>
</template>
<!-- 倒计时状态 -->
<template v-else>
<template v-else-if="countdown > 0">
{{ countdown }} 秒后可继续发送
</template>
<!-- 可再次发送 -->
<template v-else>
再次发送验证码
</template>
</button>
</template>
@@ -170,14 +178,6 @@ const countdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | null = null
/** 是否正在调用登录接口 */
const isLoggingIn = ref(false)
/** 用户最后一次输入验证码的时间戳 */
let lastOtpInputTime = 0
/** 倒计时结束后等待用户输入冷却的定时器 */
let returnWaitTimer: ReturnType<typeof setTimeout> | null = null
/** 延长倒计时的定时器(每秒递减) */
let extendTimer: ReturnType<typeof setInterval> | null = null
/** 标记60秒倒计时已结束,等待返回中 */
let countdownFinished = false
/** OTP 输入框引用 */
const otpInputRef = ref<HTMLInputElement | null>(null)
@@ -207,33 +207,6 @@ function startCountdown() {
if (countdown.value <= 0 && countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
// 倒计时结束,进入"即将返回"提示阶段
triggerReturn()
}
}, 1000)
}
/** 倒计时结束后的返回逻辑 — 考虑用户最近操作 */
function triggerReturn() {
countdownFinished = true
scheduleReturn()
}
/** 安排返回:用户停止输入5秒后回步骤一,每次输入都重置5秒 */
function scheduleReturn() {
// 清除之前所有定时器,重新开始5秒倒计时
if (returnWaitTimer) { clearTimeout(returnWaitTimer); returnWaitTimer = null }
if (extendTimer) { clearInterval(extendTimer); extendTimer = null }
// 直接重置为5秒倒计时
countdown.value = 5
extendTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
if (extendTimer) { clearInterval(extendTimer); extendTimer = null }
countdownFinished = false
step.value = 1
otpValue.value = ''
}
}, 1000)
}
@@ -276,21 +249,30 @@ async function handleSendCode() {
function handleOtpInput() {
// 过滤非数字字符
otpValue.value = otpValue.value.replace(/\D/g, '').slice(0, 6)
// 记录用户最后输入时间
lastOtpInputTime = Date.now()
// 如果倒计时已结束且用户还在输入(未满6位),重新安排返回计时
if (countdownFinished && otpValue.value.length < 6) {
scheduleReturn()
}
// 满6位自动触发登录
if (otpValue.value.length === 6) {
// 取消返回流程
if (returnWaitTimer) { clearTimeout(returnWaitTimer); returnWaitTimer = null }
if (extendTimer) { clearInterval(extendTimer); extendTimer = null }
handleLogin()
}
}
/** 步骤二:再次发送验证码 */
async function handleResendCode() {
if (isLoggingIn.value || countdown.value > 0) return
try {
const res = await sendSmsCode(phone.value)
if (res.code === '0') {
ElMessage.success('验证码已发送')
otpValue.value = ''
startCountdown()
nextTick(() => focusOtpInput())
} else {
ElMessage.error(res.msg || '验证码发送失败')
}
} catch {
// 错误已在 request 拦截器中统一处理
}
}
/** 调用登录接口 */
async function handleLogin() {
isLoggingIn.value = true