1161 lines
40 KiB
Vue
1161 lines
40 KiB
Vue
<template>
|
||
<div class="jobs-page dflex">
|
||
<SideNav />
|
||
<div class="jobs-page__content">
|
||
|
||
<div v-if="false" class="fs18 p20 bgeee">
|
||
支付
|
||
<iframe
|
||
v-if="paymentFormHtml"
|
||
:srcdoc="paymentFormHtml"
|
||
frameborder="0"
|
||
style="width: 100%; height: 600px;"
|
||
scrolling="auto"
|
||
></iframe>
|
||
</div>
|
||
|
||
<!-- 页面标题 + Tab 切换 -->
|
||
<JobPageHeader v-model:activeTab="activeTab" :favoriteCount="favoriteTotal" :applyCount="applyTotal" />
|
||
<!-- 收藏统计栏 -->
|
||
<div v-if="activeTab === 'collected'" class="jobs-page__filters-bar">
|
||
<div class="jobs-page__fav-stats">
|
||
<span class="jobs-page__fav-stats-item">有效({{ favoriteValidCount }})</span>
|
||
<span class="jobs-page__fav-stats-item">失效({{ favoriteInvalidCount }})</span>
|
||
</div>
|
||
</div>
|
||
<!-- 投递状态筛选栏 -->
|
||
<div v-if="activeTab === 'applied'" class="jobs-page__filters-bar">
|
||
<div class="jobs-page__fav-stats">
|
||
<span
|
||
v-for="tab in applyStatusTabs"
|
||
:key="tab.status"
|
||
class="jobs-page__fav-stats-item"
|
||
:class="{ 'jobs-page__fav-stats-item--active': applyStatusFilter === tab.status }"
|
||
@click="switchApplyStatus(tab.status)"
|
||
>{{ tab.label }}({{ tab.count }})</span>
|
||
</div>
|
||
</div>
|
||
<!-- 筛选条件 -->
|
||
<div v-if="isRecommendTab" class="jobs-page__filters-bar">
|
||
<div class="jobs-page__filters">
|
||
<div class="jobs-page__filter-group">
|
||
<!-- 筛选条件按钮列表(行业单独用组件) -->
|
||
<template v-for="filter in filters" :key="filter.label">
|
||
<!-- 城市筛选:使用地区选择组件(选到市级) -->
|
||
<RegionSelector
|
||
v-if="filter.key === 'city'"
|
||
:regionCodes="selectedRegionCodes"
|
||
:level="2"
|
||
:maxSelect="3"
|
||
@update:regionCodes="onRegionChange"
|
||
/>
|
||
<!-- 岗位筛选:使用岗位选择组件 -->
|
||
<JobCategorySelector
|
||
v-else-if="filter.key === 'position'"
|
||
:categoryIds="selectedCategoryIds"
|
||
:maxSelect="3"
|
||
:level="3"
|
||
:allowParentSelect="true"
|
||
@update:categoryIds="onCategoryChange"
|
||
/>
|
||
<!-- 行业筛选:使用行业选择组件 -->
|
||
<IndustrySelector
|
||
v-else-if="filter.key === 'industry'"
|
||
:industryIds="selectedIndustryIds"
|
||
:maxSelect="3"
|
||
:level="2"
|
||
:allowParentSelect="true"
|
||
@update:industryIds="onIndustryChange"
|
||
/>
|
||
<!-- 其他筛选条件 -->
|
||
<div
|
||
v-else
|
||
class="jobs-page__filter-item"
|
||
@click="handleFilterClick(filter)"
|
||
>
|
||
<span>{{ filter.selected || filter.label }}</span>
|
||
<svg class="jobs-page__filter-arrow-icon" viewBox="0 0 12 12" fill="none">
|
||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
<!-- 工作类型下拉菜单 -->
|
||
<div
|
||
v-if="filter.key === 'jobType' && showJobTypeDropdown"
|
||
class="jobs-page__filter-dropdown"
|
||
@click.stop
|
||
>
|
||
<div
|
||
class="jobs-page__filter-dropdown-item"
|
||
v-for="option in jobTypeOptions"
|
||
:key="option.value"
|
||
:class="{ 'jobs-page__filter-dropdown-item--active': filter.selected === option.label }"
|
||
@click.stop="selectJobType(filter, option)"
|
||
>
|
||
{{ option.label }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="jobs-page__search-box">
|
||
<svg class="jobs-page__search-svg" viewBox="0 0 16 16" fill="none" @click="reloadFirstPage">
|
||
<circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/>
|
||
<path d="M11 11L14 14" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||
</svg>
|
||
<input
|
||
v-model="keyword"
|
||
class="jobs-page__search-input"
|
||
placeholder="搜索职位、公司或关键词"
|
||
@keyup.enter="reloadFirstPage"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 职位列表 -->
|
||
<div ref="jobListRef" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
|
||
<div
|
||
v-for="(job, index) in jobList"
|
||
:key="index"
|
||
class="jobs-page__job-card"
|
||
:class="{ 'jobs-page__job-card--selected--none': selectedIndex === index }"
|
||
@click="goToDetail(job)"
|
||
>
|
||
<div class="jobs-page__job-main">
|
||
<!-- 左栏:职位信息 -->
|
||
<div class="jobs-page__col-left">
|
||
<!-- 职位名称 + 收藏红心 -->
|
||
<div class="jobs-page__job-title-row">
|
||
<span class="jobs-page__job-name">{{ job.title }}</span>
|
||
<!-- 收藏红心 -->
|
||
<button v-if="isRecommendTab" class="jobs-page__favorite-btn" :class="{ 'jobs-page__favorite-btn--liked': job.isFavorite }" aria-label="收藏" @click.stop="toggleFavorite(job)">
|
||
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="jobs-page__favorite-svg">
|
||
<path d="M8 13.7l-1.1-1C3.6 9.8 1.5 7.9 1.5 5.7 1.5 3.9 2.9 2.5 4.7 2.5c1 0 2 .5 2.6 1.2h1.4c.6-.7 1.6-1.2 2.6-1.2 1.8 0 3.2 1.4 3.2 3.2 0 2.2-2.1 4.1-5.4 6.9L8 13.7z" stroke="currentColor" stroke-width="1"/>
|
||
</svg>
|
||
</button>
|
||
<!-- 收藏页关闭按钮 -->
|
||
<button v-if="activeTab === 'collected'" class="jobs-page__job-remove" aria-label="取消收藏" @click.stop="removeFavoriteFromList(job, index)">
|
||
<svg viewBox="0 0 16 16" fill="none" class="jobs-page__remove-svg">
|
||
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<!-- 职位标签 -->
|
||
<div class="jobs-page__job-tags">
|
||
<span v-for="(tag, ti) in job.tags" :key="ti" class="jobs-page__job-tag">{{ tag }}</span>
|
||
</div>
|
||
<!-- 底部:地区 + 学历 + 招聘分类 -->
|
||
<div class="jobs-page__job-meta-bottom">
|
||
<span class="jobs-page__meta-location">
|
||
<svg viewBox="0 0 18 18" fill="none" class="jobs-page__location-icon">
|
||
<path d="M9 1.5C5.96 1.5 3.5 3.96 3.5 7c0 4.13 5.5 9.5 5.5 9.5s5.5-5.37 5.5-9.5c0-3.04-2.46-5.5-5.5-5.5zm0 7.5a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/>
|
||
</svg>
|
||
{{ job.regionName }}
|
||
</span>
|
||
<span class="jobs-page__meta-edu">本科</span>
|
||
<span v-if="formatEmploymentType(job.recruitCategory)" class="jobs-page__meta-recruit">{{ formatEmploymentType(job.recruitCategory) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中栏:公司信息(居中) -->
|
||
<div class="jobs-page__col-center">
|
||
<div class="jobs-page__company-info">
|
||
<!-- 公司logo + 公司名 -->
|
||
<div class="jobs-page__company-row">
|
||
<div class="jobs-page__company-logo">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||
<path d="M0 0h24v24H0z" fill="none" />
|
||
<path fill="currentColor" d="M18 15h-2v2h2m0-6h-2v2h2m2 6h-8v-2h2v-2h-2v-2h2v-2h-2V9h8M10 7H8V5h2m0 6H8V9h2m0 6H8v-2h2m0 6H8v-2h2M6 7H4V5h2m0 6H4V9h2m0 6H4v-2h2m0 6H4v-2h2m6-10V3H2v18h20V7z" />
|
||
</svg>
|
||
</div>
|
||
<span class="jobs-page__company-name">{{ job.companyShortName || job.companyName }}</span>
|
||
</div>
|
||
<!-- 公司标签 -->
|
||
<div v-if="job.companyTags && job.companyTags.length" class="jobs-page__company-tags">
|
||
<span v-for="(ctag, ci) in job.companyTags" :key="ci" class="jobs-page__company-tag">{{ ctag }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dflex-end flex1">
|
||
<!-- 右栏:操作按钮 + 不再显示 -->
|
||
<div class="jobs-page__col-right mr10">
|
||
<!-- 按钮行 -->
|
||
<div class="jobs-page__job-action-right">
|
||
<button class="jobs-page__job-helper" @click.stop="askAssistant(job)">问Nova</button>
|
||
<button @click.stop="handleReport(job.sourceUrl)" class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">去投递</button>
|
||
</div>
|
||
<!-- 不再显示 -->
|
||
<span v-if="isRecommendTab" class="jobs-page__hide-btn" @click.stop="openDislikeDialog(job)">不再显示</span>
|
||
</div>
|
||
|
||
<!-- 最右侧:匹配度 -->
|
||
<div class="jobs-page__job-match" :class="isAuthenticated ? matchClass(job.matchScore) : 'jobs-page__job-match--locked'">
|
||
<template v-if="isAuthenticated">
|
||
<div class="jobs-page__match-ring">
|
||
<svg viewBox="0 0 80 80" class="jobs-page__ring-svg">
|
||
<circle cx="40" cy="40" r="34" stroke-width="5" stroke="#9FD051" fill="none" opacity="0.3"/>
|
||
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
|
||
:stroke="matchRingColor(job.matchScore)"
|
||
stroke-linecap="round"
|
||
:stroke-dasharray="2 * Math.PI * 34"
|
||
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
|
||
transform="rotate(-90 40 40)"
|
||
/>
|
||
</svg>
|
||
<div class="jobs-page__match-score">{{ job.matchScore }}%</div>
|
||
</div>
|
||
<div class="jobs-page__match-level">{{ matchLevelText(job.matchScore) }}</div>
|
||
</template>
|
||
<template v-else>
|
||
<svg class="jobs-page__lock-icon" viewBox="0 0 24 24" fill="none">
|
||
<rect x="5" y="11" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<path d="M8 11V7a4 4 0 118 0v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||
</svg>
|
||
<div class="jobs-page__match-lock-text">登录查看匹配值</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 弹出菜单 -->
|
||
<div v-if="isRecommendTab && job.showMenu" class="jobs-page__job-popup" @click.stop>
|
||
<div
|
||
class="jobs-page__job-popup-item"
|
||
@click.stop="handlePopupAction('从列表中移除', job)"
|
||
>不再显示</div>
|
||
</div>
|
||
</div>
|
||
<!-- 首次加载中提示 -->
|
||
<div v-if="loading" class="jobs-page__loading-box">
|
||
<div class="jobs-page__loading-spinner"></div>
|
||
<span>加载中...</span>
|
||
</div>
|
||
<!-- 暂无数据提示 -->
|
||
<div v-if="!loading && jobList.length === 0" class="jobs-page__empty">暂无数据</div>
|
||
<!-- 加载更多提示 -->
|
||
<div v-if="loadingMore" class="jobs-page__loading-box">
|
||
<div class="jobs-page__loading-spinner"></div>
|
||
<span>加载中...</span>
|
||
</div>
|
||
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多符合的职位了</div>
|
||
</div>
|
||
</div>
|
||
<AiChat :jobId="currentAskJobId" />
|
||
|
||
<!-- 职位不感兴趣反馈弹窗 -->
|
||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
||
|
||
<!-- 职位问题反馈弹窗 -->
|
||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
|
||
|
||
<!-- AI助手投递提醒弹窗 -->
|
||
<div v-if="showAgentRemindDialog" class="jobs-page__agent-remind-overlay" @click.self="closeAgentRemind">
|
||
<div class="jobs-page__agent-remind-dialog">
|
||
<p class="jobs-page__agent-remind-text">去AI助手投递岗位效果更好哦!</p>
|
||
<div class="jobs-page__agent-remind-actions">
|
||
<button class="jobs-page__agent-remind-btn jobs-page__agent-remind-btn--secondary" @click="directApply">直接投</button>
|
||
<button class="jobs-page__agent-remind-btn jobs-page__agent-remind-btn--primary" @click="goToAgent">去AI助手</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { useStore } from 'vuex'
|
||
import { JOB_TYPE_OPTIONS, formatEmploymentType } from '@/stores/index'
|
||
import SideNav from '@/components/SideNav.vue'
|
||
import AiChat from '@/components/AiChat.vue'
|
||
import JobPageHeader from '@/components/JobPageHeader.vue'
|
||
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
|
||
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||
|
||
|
||
import { fetchJobList, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList } from '@/api/jobs'
|
||
import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData } from '@/api/jobs'
|
||
import { getTimeEvent, setTimeEvent, timestampToLocalDateTime } from '@/utils/time'
|
||
|
||
|
||
// 2. 注意:这里的字符串不需要再手动转义双引号了
|
||
const paymentFormHtml = ref(`
|
||
<form name="punchout_form" method="post" action="https://openapi-sandbox.dl.alipaydev.com/gateway.do?charset=UTF-8&method=alipay.trade.page.pay&sign=o8MQ7cEAh4yrf6ri5iT9gXbtR%2BTncX36p9JNZf8nmXxEp1WKO6dF1HDC6DPF8nxJbus6OG9E5UPSoiHgqrEiu3aeOs4LMcYzXXZP0UQzG%2F6pXyZrvlL70G8uwpnV6MgQojzhKheG0vYIJgSPaGsHKjwcp%2BT8yOZg3eEDIgFYEBf2a3c7kANc7%2FuOOGCSD9Ip9zqwX9LQ3b433NPH3AbGQkFSLsdGOTY62I7yI8hEF7EM7nNm%2BYryieqju21yN76JrV4k%2BgIGHn83I5oT3y7zurzdTqHLD4UawpPWdJ5B7p6wnJ4cN4uzO8JgRpNqogYqqKBPcHIlB9o4uax3bcRqhg%3D%3D¬ify_url=http%3A%2F%2F39.108.134.218%3A10106%2Fapi%2F%2Fpublic%2FalipayNotify%2Fpay&version=1.0&app_id=2018062160374941&sign_type=RSA2×tamp=2026-05-20+15%3A29%3A21&alipay_sdk=alipay-sdk-java-4.40.791.ALL&format=json">
|
||
<input type="hidden" name="biz_content" value='{"out_trade_no":"OP2057000709638877186","product_code":"FAST_INSTANT_TRADE_PAY","qr_pay_mode":"4","qrcode_width":200,"subject":"周会员","total_amount":"0.10"}'>
|
||
<input type="submit" value="立即支付" style="display:none">
|
||
</form>
|
||
<script>document.forms[0].submit();<\/script>
|
||
`)
|
||
|
||
// ==================== 路由相关 ====================
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const store = useStore()
|
||
|
||
/** 登录状态 — 从 store 读取 */
|
||
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||
|
||
/** 用户昵称 — 从全局 store 读取 */
|
||
const nickName = computed(() => store.state.userInfo?.nick || '')
|
||
|
||
// ==================== 页面状态 ====================
|
||
|
||
/** 当前激活的 Tab,从 URL query 参数读取,默认"推荐" */
|
||
const activeTab = ref((route.query.tab as string) || 'recommend')
|
||
|
||
/** 是否为推荐列表 Tab */
|
||
const isRecommendTab = computed(() => activeTab.value === 'recommend')
|
||
|
||
/** 搜索框输入内容 */
|
||
const keyword = ref('')
|
||
|
||
/** 当前选中的职位卡片索引 */
|
||
const selectedIndex = ref(0)
|
||
|
||
/** 不感兴趣弹窗的显示状态 */
|
||
const showDislikeDialog = ref(false)
|
||
|
||
/** 当前操作的职位 ID(用于提交不感兴趣反馈) */
|
||
const dislikeJobId = ref<string | null>(null)
|
||
|
||
/** 问题反馈弹窗的显示状态 */
|
||
const showFeedbackDialog = ref(false)
|
||
|
||
/** 当前操作的职位 ID(用于提交问题反馈) */
|
||
const feedbackJobId = ref<string | null>(null)
|
||
|
||
/** 当前问助手的岗位 ID(传给 AiChat 组件) */
|
||
const currentAskJobId = ref<string>('')
|
||
|
||
|
||
|
||
/** 点击"问助手"按钮,传入岗位 ID 给 AiChat */
|
||
function askAssistant(job: JobItem) {
|
||
currentAskJobId.value = job.id
|
||
}
|
||
|
||
// ==================== 收藏统计 ====================
|
||
|
||
/** 收藏总数(用于 Tab 标签显示) */
|
||
const favoriteTotal = ref(0)
|
||
|
||
/** 有效收藏数 */
|
||
const favoriteValidCount = ref(0)
|
||
|
||
/** 失效收藏数 */
|
||
const favoriteInvalidCount = ref(0)
|
||
|
||
/** 加载收藏统计 */
|
||
async function loadFavoriteCount() {
|
||
try {
|
||
const res = await fetchFavoriteCount()
|
||
if (res.code === '0' && res.data) {
|
||
favoriteTotal.value = res.data.totalCount
|
||
favoriteValidCount.value = res.data.validCount
|
||
favoriteInvalidCount.value = res.data.invalidCount
|
||
}
|
||
} catch (e) {
|
||
console.error('加载收藏统计失败', e)
|
||
}
|
||
}
|
||
|
||
// ==================== 投递统计 ====================
|
||
|
||
/** 投递总数(用于 Tab 标签显示) */
|
||
const applyTotal = ref(0)
|
||
|
||
/** 投递统计详情 */
|
||
const applyCountData = ref<ApplyCountData>({
|
||
totalCount: 0,
|
||
appliedCount: 0,
|
||
interviewingCount: 0,
|
||
offerCount: 0,
|
||
rejectedCount: 0,
|
||
closedCount: 0,
|
||
})
|
||
|
||
/** 当前投递状态筛选(null=全部) */
|
||
const applyStatusFilter = ref<number | null>(null)
|
||
|
||
/** 投递状态 Tab 列表 */
|
||
const applyStatusTabs = computed(() => [
|
||
{ status: 0, label: '已投递', count: applyCountData.value.appliedCount },
|
||
{ status: 1, label: '面试中', count: applyCountData.value.interviewingCount },
|
||
{ status: 2, label: '有Offer', count: applyCountData.value.offerCount },
|
||
{ status: 3, label: '未通过', count: applyCountData.value.rejectedCount },
|
||
{ status: 4, label: '已结束', count: applyCountData.value.closedCount },
|
||
])
|
||
|
||
/** 切换投递状态筛选 */
|
||
function switchApplyStatus(status: number) {
|
||
applyStatusFilter.value = applyStatusFilter.value === status ? null : status
|
||
pageNum.value = 1
|
||
hasNoMoreData.value = false
|
||
jobList.value = []
|
||
total.value = 0
|
||
loadApplyList()
|
||
}
|
||
|
||
/** 加载投递统计 */
|
||
async function loadApplyCount() {
|
||
try {
|
||
const res = await fetchApplyCount()
|
||
if (res.code === '0' && res.data) {
|
||
applyCountData.value = res.data
|
||
applyTotal.value = res.data.totalCount
|
||
}
|
||
} catch (e) {
|
||
console.error('加载投递统计失败', e)
|
||
}
|
||
}
|
||
|
||
// ==================== 常量数据 ====================
|
||
|
||
/** 筛选条件项类型 */
|
||
interface FilterItem {
|
||
label: string
|
||
key: string
|
||
selected: string
|
||
}
|
||
|
||
/** 筛选条件列表 */
|
||
const filters = ref<FilterItem[]>([
|
||
{ label: '城市', key: 'city', selected: '' },
|
||
{ label: '岗位', key: 'position', selected: '' },
|
||
{ label: '行业', key: 'industry', selected: '' },
|
||
{ label: '招聘分类', key: 'jobType', selected: '' },
|
||
])
|
||
|
||
/** 招聘分类选项映射:从全局 store 常量统一引入 */
|
||
const jobTypeOptions = JOB_TYPE_OPTIONS
|
||
|
||
/** 招聘分类下拉菜单是否显示 */
|
||
const showJobTypeDropdown = ref(false)
|
||
|
||
/** 当前选中的招聘分类 — 直接读 store.jobIntention.recruitCategory */
|
||
const selectedEmploymentType = computed<number | null>(
|
||
() => store.state.jobIntention.recruitCategory ?? null,
|
||
)
|
||
|
||
/** 选中的行业 id 数组 — 直接读 store.jobIntention.industryIds */
|
||
const selectedIndustryIds = computed<number[]>(
|
||
() => store.state.jobIntention.industryIds || [],
|
||
)
|
||
|
||
/** 选中的岗位 id 数组 — 直接读 store.jobIntention.categoryIds */
|
||
const selectedCategoryIds = computed<number[]>(
|
||
() => store.state.jobIntention.categoryIds || [],
|
||
)
|
||
|
||
/** 选中的地区编码数组 — 直接读 store.jobIntention.regionCodes */
|
||
const selectedRegionCodes = computed<string[]>(
|
||
() => store.state.jobIntention.regionCodes || [],
|
||
)
|
||
|
||
/** 行业选择变更回调 */
|
||
function onIndustryChange(ids: number[]) {
|
||
store.dispatch('saveJobIntention', {
|
||
...store.state.jobIntention,
|
||
industryIds: ids,
|
||
})
|
||
}
|
||
|
||
/** 岗位选择变更回调 */
|
||
function onCategoryChange(ids: number[]) {
|
||
store.dispatch('saveJobIntention', {
|
||
...store.state.jobIntention,
|
||
categoryIds: ids,
|
||
})
|
||
}
|
||
|
||
/** 地区选择变更回调 */
|
||
function onRegionChange(codes: string[]) {
|
||
store.dispatch('saveJobIntention', {
|
||
...store.state.jobIntention,
|
||
regionCodes: codes,
|
||
})
|
||
}
|
||
|
||
/** 点击筛选条件按钮 — 仅招聘分类展开下拉 */
|
||
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,
|
||
recruitCategory: option.value,
|
||
})
|
||
}
|
||
|
||
/** 监听 store 中 recruitCategory 变化,同步招聘分类筛选按钮的显示文字 */
|
||
watch(selectedEmploymentType, (val) => {
|
||
const jobTypeFilter = filters.value.find(f => f.key === 'jobType')
|
||
if (jobTypeFilter && val !== null) {
|
||
const matched = jobTypeOptions.find(o => o.value === val)
|
||
if (matched) jobTypeFilter.selected = matched.label
|
||
}
|
||
}, { immediate: true })
|
||
|
||
/** 点击页面其他区域时关闭下拉菜单 */
|
||
function closeDropdownOnClickOutside(e: MouseEvent) {
|
||
const target = e.target as HTMLElement
|
||
if (!target.closest('.jobs-page__filter-item')) {
|
||
showJobTypeDropdown.value = false
|
||
}
|
||
}
|
||
|
||
/** 跳转到原链接 — 带 AI 助手提醒弹窗频率控制 */
|
||
/**
|
||
* 提醒弹窗频率逻辑说明:
|
||
* 1. 未登录用户:直接跳转外部链接,不弹窗
|
||
* 2. 已登录用户首次点击(无 ai_agent_remind_first 缓存):弹窗提醒,记录第一次提醒时间
|
||
* 3. 有第一次记录但无第二次记录:
|
||
* - 距第一次记录 ≤ 1小时:直接跳转,不弹窗
|
||
* - 距第一次记录 > 1小时:弹窗提醒,记录第二次提醒时间
|
||
* 4. 有第二次记录:
|
||
* - 距第二次记录 ≤ 7天:直接跳转,不弹窗
|
||
* - 距第二次记录 > 7天:弹窗提醒,更新第二次记录时间为当前时间
|
||
*/
|
||
function handleReport(url: string) {
|
||
// 暂存当前要跳转的链接
|
||
pendingApplyUrl.value = url
|
||
|
||
// 未登录直接跳转
|
||
if (!isAuthenticated.value) {
|
||
window.open(url, '_blank')
|
||
return
|
||
}
|
||
|
||
const userId = store.state.userInfo?.id
|
||
if (!userId) {
|
||
window.open(url, '_blank')
|
||
return
|
||
}
|
||
|
||
const now = Date.now()
|
||
const firstTime = getTimeEvent('ai_agent_remind_first', userId)
|
||
const secondTime = getTimeEvent('ai_agent_remind_second', userId)
|
||
|
||
if (!firstTime) {
|
||
// 情况1:从未弹过 → 弹窗,记录第一次时间
|
||
setTimeEvent('ai_agent_remind_first', userId, timestampToLocalDateTime(now))
|
||
showAgentRemindDialog.value = true
|
||
return
|
||
}
|
||
|
||
if (!secondTime) {
|
||
// 情况2:有第一次记录,无第二次记录 → 检查是否超过1小时
|
||
const firstTimestamp = new Date(firstTime).getTime()
|
||
const diffHours = (now - firstTimestamp) / (1000 * 60 * 60)
|
||
if (diffHours <= 1) {
|
||
// 1小时内不弹窗,直接跳转
|
||
window.open(url, '_blank')
|
||
} else {
|
||
// 超过1小时 → 弹窗,记录第二次时间
|
||
setTimeEvent('ai_agent_remind_second', userId, timestampToLocalDateTime(now))
|
||
showAgentRemindDialog.value = true
|
||
}
|
||
return
|
||
}
|
||
|
||
// 情况3:有第二次记录 → 检查是否超过7天
|
||
const secondTimestamp = new Date(secondTime).getTime()
|
||
const diffDays = (now - secondTimestamp) / (1000 * 60 * 60 * 24)
|
||
if (diffDays <= 7) {
|
||
// 7天内不弹窗,直接跳转
|
||
window.open(url, '_blank')
|
||
} else {
|
||
// 超过7天 → 弹窗,更新第二次记录时间
|
||
setTimeEvent('ai_agent_remind_second', userId, timestampToLocalDateTime(now))
|
||
showAgentRemindDialog.value = true
|
||
}
|
||
}
|
||
|
||
// ==================== AI助手提醒弹窗状态 ====================
|
||
|
||
/** AI助手提醒弹窗是否显示 */
|
||
const showAgentRemindDialog = ref(false)
|
||
|
||
/** 暂存待跳转的外部链接 */
|
||
const pendingApplyUrl = ref('')
|
||
|
||
/** 关闭AI助手提醒弹窗 */
|
||
function closeAgentRemind() {
|
||
showAgentRemindDialog.value = false
|
||
pendingApplyUrl.value = ''
|
||
}
|
||
|
||
/** 点击"直接投"按钮 — 关闭弹窗并跳转外部链接 */
|
||
function directApply() {
|
||
showAgentRemindDialog.value = false
|
||
if (pendingApplyUrl.value) {
|
||
window.open(pendingApplyUrl.value, '_blank')
|
||
}
|
||
pendingApplyUrl.value = ''
|
||
}
|
||
|
||
/** 点击"去AI助手"按钮 — 关闭弹窗并跳转到 Agent 页面 */
|
||
function goToAgent() {
|
||
showAgentRemindDialog.value = false
|
||
pendingApplyUrl.value = ''
|
||
router.push('/agent')
|
||
}
|
||
|
||
onMounted(async () => {
|
||
document.addEventListener('click', closeDropdownOnClickOutside)
|
||
|
||
// 先判断是否有缓存(从详情页返回),在任何异步操作之前取出
|
||
const cache = store.state.jobListCache
|
||
const hasCache = !!(cache && cache.list.length > 0)
|
||
|
||
if (hasCache) {
|
||
// 立即恢复缓存数据,隐藏列表避免闪烁
|
||
restoring.value = true
|
||
jobList.value = cache!.list
|
||
pageNum.value = cache!.pageNum
|
||
total.value = cache!.total
|
||
savedScrollTop = cache!.scrollTop
|
||
// 清除缓存,避免下次非详情页返回时误用
|
||
store.commit('SET_JOB_LIST_CACHE', null)
|
||
}
|
||
|
||
// 进入 Jobs 页时加载公共工具数据(行业分类等)
|
||
store.dispatch('loadCommonData')
|
||
|
||
// 仅登录状态下才加载需要鉴权的数据
|
||
if (isAuthenticated.value) {
|
||
// 加载用户个人信息到全局 store
|
||
store.dispatch('loadUserInfo')
|
||
|
||
|
||
|
||
// 加载收藏统计(用于 Tab 标签显示)
|
||
loadFavoriteCount()
|
||
|
||
// 加载投递统计(用于 Tab 标签显示)
|
||
loadApplyCount()
|
||
|
||
// 加载求职意向数据,computed 会自动反映到筛选条件
|
||
await store.dispatch('loadJobIntention')
|
||
}
|
||
|
||
if (hasCache) {
|
||
// 从详情页返回,恢复滚动位置后再显示列表
|
||
nextTick(() => {
|
||
if (jobListRef.value) {
|
||
jobListRef.value.scrollTop = savedScrollTop
|
||
}
|
||
restoring.value = false
|
||
})
|
||
} else {
|
||
// 没有缓存,正常加载
|
||
loadCurrentTab()
|
||
}
|
||
|
||
// 初始化完成,允许 watcher 正常工作
|
||
// 使用双重 nextTick 确保 loadJobIntention 引起的所有响应式更新都已 flush
|
||
nextTick(() => {
|
||
nextTick(() => {
|
||
initializing = false
|
||
})
|
||
})
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
document.removeEventListener('click', closeDropdownOnClickOutside)
|
||
})
|
||
|
||
// ==================== 职位列表项(扩展接口字段,增加前端交互状态) ====================
|
||
|
||
/** 职位列表项类型(在接口返回基础上扩展前端交互字段) */
|
||
interface JobItem extends JobListItem {
|
||
/** 是否已投递 */
|
||
applied: boolean
|
||
/** 是否显示弹出菜单 */
|
||
showMenu: boolean
|
||
}
|
||
|
||
// ==================== 分页与加载状态 ====================
|
||
|
||
/** 当前页码 */
|
||
const pageNum = ref(1)
|
||
|
||
/** 每页条数 */
|
||
const pageSize = ref(15)
|
||
|
||
/** 总记录数 */
|
||
const total = ref(0)
|
||
|
||
/** 是否正在加载(首次加载) */
|
||
const loading = ref(false)
|
||
|
||
/** 是否正在加载下一页 */
|
||
const loadingMore = ref(false)
|
||
|
||
/** 接口返回空列表时标记为无更多数据 */
|
||
const hasNoMoreData = ref(false)
|
||
|
||
/** 是否已加载全部数据 */
|
||
const noMore = computed(() => hasNoMoreData.value || (jobList.value.length >= total.value && total.value > 0))
|
||
|
||
/** 职位列表数据 */
|
||
const jobList = ref<JobItem[]>([])
|
||
|
||
/** 列表容器 ref,用于监听滚动和恢复滚动位置 */
|
||
const jobListRef = ref<HTMLElement | null>(null)
|
||
|
||
/** 记录离开页面前的滚动位置 */
|
||
let savedScrollTop = 0
|
||
|
||
/** 初始化标志 — onMounted 期间为 true,防止 watcher 误触发 reloadFirstPage */
|
||
let initializing = true
|
||
|
||
/** 恢复缓存中 — 为 true 时列表容器 visibility:hidden,避免滚动位置闪烁 */
|
||
const restoring = ref(false)
|
||
// ==================== 加载岗位列表 ====================
|
||
|
||
/** 组装请求参数 */
|
||
function buildParams(): JobListParams {
|
||
const params: JobListParams = {
|
||
pageNum: pageNum.value,
|
||
pageSize: pageSize.value,
|
||
}
|
||
// 地区筛选
|
||
if (selectedRegionCodes.value.length) {
|
||
params.regionCodes = selectedRegionCodes.value
|
||
}
|
||
// 岗位类型筛选
|
||
if (selectedCategoryIds.value.length) {
|
||
params.categoryIds = selectedCategoryIds.value
|
||
}
|
||
// 行业筛选
|
||
if (selectedIndustryIds.value.length) {
|
||
params.industryIds = selectedIndustryIds.value
|
||
}
|
||
// 招聘分类筛选
|
||
if (selectedEmploymentType.value !== null) {
|
||
params.recruitCategory = selectedEmploymentType.value
|
||
}
|
||
// 搜索关键词
|
||
if (keyword.value.trim()) {
|
||
params.keyword = keyword.value.trim()
|
||
}
|
||
return params
|
||
}
|
||
|
||
/** 加载岗位列表数据(首次加载,替换列表) */
|
||
async function loadJobList() {
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchJobList(buildParams())
|
||
if (res.code === '0' && res.data) {
|
||
// 将接口数据映射为前端 JobItem,补充交互状态字段
|
||
jobList.value = res.data.list.map((item) => ({
|
||
...item,
|
||
applied: false,
|
||
showMenu: false,
|
||
}))
|
||
total.value = Number(res.data.total)
|
||
}
|
||
} catch (e) {
|
||
console.error('加载岗位列表失败', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 加载下一页数据(追加到列表末尾) */
|
||
async function loadNextPage() {
|
||
if (loadingMore.value || noMore.value) return
|
||
loadingMore.value = true
|
||
pageNum.value++
|
||
try {
|
||
const res = await fetchJobList(buildParams())
|
||
if (res.code === '0' && res.data) {
|
||
const newItems = res.data.list.map((item) => ({
|
||
...item,
|
||
applied: false,
|
||
showMenu: false,
|
||
}))
|
||
// 返回空列表时标记无更多数据,停止滚动加载
|
||
if (newItems.length === 0) {
|
||
hasNoMoreData.value = true
|
||
pageNum.value--
|
||
} else {
|
||
jobList.value.push(...newItems)
|
||
total.value = Number(res.data.total)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 加载失败时回退页码
|
||
pageNum.value--
|
||
console.error('加载下一页失败', e)
|
||
} finally {
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
/** 列表滚动事件 — 滚动到底部时自动加载下一页 */
|
||
function onListScroll() {
|
||
const el = jobListRef.value
|
||
if (!el) return
|
||
// 距离底部小于 100px 时触发加载
|
||
const threshold = 100
|
||
if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) {
|
||
if (isRecommendTab.value) {
|
||
loadNextPage()
|
||
} else if (activeTab.value === 'collected') {
|
||
loadFavoriteNextPage()
|
||
} else if (activeTab.value === 'applied') {
|
||
loadApplyNextPage()
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 筛选条件变化时重置到第一页并重新加载 */
|
||
function reloadFirstPage() {
|
||
pageNum.value = 1
|
||
hasNoMoreData.value = false
|
||
jobList.value = []
|
||
total.value = 0
|
||
// 筛选条件变化时清除缓存
|
||
store.commit('SET_JOB_LIST_CACHE', null)
|
||
loadCurrentTab()
|
||
}
|
||
|
||
/** 根据当前 Tab 加载对应列表 */
|
||
function loadCurrentTab() {
|
||
if (isRecommendTab.value) {
|
||
loadJobList()
|
||
} else if (activeTab.value === 'collected') {
|
||
loadFavoriteList()
|
||
} else if (activeTab.value === 'applied') {
|
||
loadApplyList()
|
||
}
|
||
}
|
||
|
||
/** 加载收藏列表 */
|
||
async function loadFavoriteList() {
|
||
loading.value = true
|
||
try {
|
||
const params: FavoriteListParams = {
|
||
pageNum: pageNum.value,
|
||
pageSize: pageSize.value,
|
||
valid: true,
|
||
}
|
||
const res = await fetchFavoriteList(params)
|
||
if (res.code === '0' && res.data) {
|
||
jobList.value = res.data.list.map((item) => ({
|
||
...item,
|
||
applied: false,
|
||
showMenu: false,
|
||
}))
|
||
total.value = Number(res.data.total)
|
||
}
|
||
} catch (e) {
|
||
console.error('加载收藏列表失败', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 加载收藏列表下一页 */
|
||
async function loadFavoriteNextPage() {
|
||
if (loadingMore.value || noMore.value) return
|
||
loadingMore.value = true
|
||
pageNum.value++
|
||
try {
|
||
const params: FavoriteListParams = {
|
||
pageNum: pageNum.value,
|
||
pageSize: pageSize.value,
|
||
valid: true,
|
||
}
|
||
const res = await fetchFavoriteList(params)
|
||
if (res.code === '0' && res.data) {
|
||
const newItems = res.data.list.map((item) => ({
|
||
...item,
|
||
applied: false,
|
||
showMenu: false,
|
||
}))
|
||
// 返回空列表时标记无更多数据,停止滚动加载
|
||
if (newItems.length === 0) {
|
||
hasNoMoreData.value = true
|
||
pageNum.value--
|
||
} else {
|
||
jobList.value.push(...newItems)
|
||
total.value = Number(res.data.total)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
pageNum.value--
|
||
console.error('加载收藏列表下一页失败', e)
|
||
} finally {
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
// ==================== 投递列表加载 ====================
|
||
|
||
/** 加载投递列表 */
|
||
async function loadApplyList() {
|
||
loading.value = true
|
||
try {
|
||
const params: ApplyListParams = {
|
||
pageNum: pageNum.value,
|
||
pageSize: pageSize.value,
|
||
}
|
||
if (applyStatusFilter.value !== null) {
|
||
params.status = applyStatusFilter.value
|
||
}
|
||
const res = await fetchApplyList(params)
|
||
if (res.code === '0' && res.data) {
|
||
jobList.value = res.data.list.map((item) => ({
|
||
...item,
|
||
applied: true,
|
||
showMenu: false,
|
||
}))
|
||
total.value = Number(res.data.total)
|
||
}
|
||
} catch (e) {
|
||
console.error('加载投递列表失败', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 加载投递列表下一页 */
|
||
async function loadApplyNextPage() {
|
||
if (loadingMore.value || noMore.value) return
|
||
loadingMore.value = true
|
||
pageNum.value++
|
||
try {
|
||
const params: ApplyListParams = {
|
||
pageNum: pageNum.value,
|
||
pageSize: pageSize.value,
|
||
}
|
||
if (applyStatusFilter.value !== null) {
|
||
params.status = applyStatusFilter.value
|
||
}
|
||
const res = await fetchApplyList(params)
|
||
if (res.code === '0' && res.data) {
|
||
const newItems = res.data.list.map((item) => ({
|
||
...item,
|
||
applied: true,
|
||
showMenu: false,
|
||
}))
|
||
// 返回空列表时标记无更多数据,停止滚动加载
|
||
if (newItems.length === 0) {
|
||
hasNoMoreData.value = true
|
||
pageNum.value--
|
||
} else {
|
||
jobList.value.push(...newItems)
|
||
total.value = Number(res.data.total)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
pageNum.value--
|
||
console.error('加载投递列表下一页失败', e)
|
||
} finally {
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
// 监听筛选条件变化,自动重新加载
|
||
watch(
|
||
[selectedRegionCodes, selectedCategoryIds, selectedIndustryIds, selectedEmploymentType],
|
||
() => {
|
||
// 初始化阶段(loadJobIntention 导致的变化)跳过,避免清掉缓存
|
||
if (initializing) return
|
||
reloadFirstPage()
|
||
},
|
||
{ deep: true },
|
||
)
|
||
|
||
|
||
|
||
// 监听 Tab 切换,重置列表状态并加载对应数据
|
||
watch(activeTab, (newTab, oldTab) => {
|
||
if (initializing) return
|
||
|
||
// 收藏和投递 Tab 需要登录
|
||
if ((newTab === 'collected' || newTab === 'applied') && !isAuthenticated.value) {
|
||
// 回退到之前的 Tab,弹登录框
|
||
nextTick(() => { activeTab.value = oldTab })
|
||
store.dispatch('openLogin')
|
||
return
|
||
}
|
||
|
||
jobList.value = []
|
||
keyword.value = ''
|
||
pageNum.value = 1
|
||
total.value = 0
|
||
hasNoMoreData.value = false
|
||
loadCurrentTab()
|
||
// 切换到收藏 Tab 时刷新统计
|
||
if (newTab === 'collected') {
|
||
loadFavoriteCount()
|
||
}
|
||
// 切换到投递 Tab 时刷新统计并重置筛选
|
||
if (newTab === 'applied') {
|
||
applyStatusFilter.value = null
|
||
loadApplyCount()
|
||
}
|
||
})
|
||
|
||
// ==================== 事件处理 ====================
|
||
|
||
/** 切换职位卡片的弹出菜单(同时关闭其他已打开的菜单) */
|
||
function toggleMenu(index: number) {
|
||
jobList.value.forEach((j, i) => {
|
||
j.showMenu = i === index ? !j.showMenu : false
|
||
})
|
||
}
|
||
|
||
/** 处理弹出菜单操作项点击 */
|
||
function handlePopupAction(action: string, job: JobItem) {
|
||
job.showMenu = false
|
||
if (action === '从列表中移除') {
|
||
handleRemoveFromList(job)
|
||
} else if (action === '复制链接') {
|
||
handleCopyLink(job)
|
||
} else if (action === '问题反馈') {
|
||
openFeedbackDialog(job)
|
||
}
|
||
}
|
||
|
||
/** 复制职位原链接到剪贴板 */
|
||
async function handleCopyLink(job: JobItem) {
|
||
try {
|
||
await navigator.clipboard.writeText(job.sourceUrl)
|
||
ElMessage.success('复制链接成功')
|
||
} catch {
|
||
ElMessage.error('复制链接失败')
|
||
}
|
||
}
|
||
|
||
/** 从推荐列表中移除职位(前端移除 + 预留接口调用) */
|
||
async function handleRemoveFromList(job: JobItem) {
|
||
try {
|
||
// TODO: 接口对接后此处会真正调用后端,当前为模拟成功
|
||
const res = await removeJobFromList(job.id)
|
||
if (res.code === '0') {
|
||
const index = jobList.value.findIndex(j => j.id === job.id)
|
||
if (index !== -1) {
|
||
jobList.value.splice(index, 1)
|
||
total.value = Math.max(0, total.value - 1)
|
||
}
|
||
ElMessage.success('已从列表中移除')
|
||
}
|
||
} catch (e) {
|
||
console.error('移除职位失败', e)
|
||
}
|
||
}
|
||
|
||
/** 根据匹配分数返回对应的 CSS 类名 */
|
||
function matchClass(score: number) {
|
||
return score >= 80 ? 'jobs-page__job-match--high' : 'jobs-page__job-match--low'
|
||
}
|
||
|
||
/** 根据匹配分数返回环形颜色 */
|
||
function matchRingColor(score: number) {
|
||
if (score >= 80) return '#9FD051'
|
||
return '#9FD051'
|
||
}
|
||
|
||
/** 根据匹配分数返回匹配等级文案 */
|
||
function matchLevelText(score: number) {
|
||
if (score >= 80) return '高匹配度'
|
||
if (score >= 60) return '一般匹配'
|
||
return '匹配度偏低'
|
||
}
|
||
|
||
/** 跳转到岗位详情页 — 未登录弹登录框,登录后再跳转 */
|
||
function goToDetail(job: JobItem) {
|
||
if (!isAuthenticated.value) {
|
||
store.dispatch('openLogin', `/jobs/${job.id}`)
|
||
return
|
||
}
|
||
// 保存当前列表状态到 store,返回时恢复
|
||
store.commit('SET_JOB_LIST_CACHE', {
|
||
list: jobList.value,
|
||
pageNum: pageNum.value,
|
||
total: total.value,
|
||
scrollTop: jobListRef.value?.scrollTop ?? 0,
|
||
})
|
||
router.push(`/jobs/${job.id}`)
|
||
}
|
||
|
||
/** 不感兴趣弹窗和问题反馈弹窗的组件引用 */
|
||
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
|
||
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
|
||
|
||
/** 收藏/取消收藏岗位 */
|
||
async function toggleFavorite(job: JobItem) {
|
||
try {
|
||
const res = job.isFavorite
|
||
? await removeJobFavorite(job.id)
|
||
: await toggleJobFavorite(job.id)
|
||
if (res.code === '0') {
|
||
const wasFavorite = job.isFavorite
|
||
job.isFavorite = !job.isFavorite
|
||
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
|
||
// 刷新收藏统计
|
||
loadFavoriteCount()
|
||
}
|
||
} catch (e) {
|
||
console.error('收藏操作失败', e)
|
||
}
|
||
}
|
||
|
||
/** 收藏列表中取消收藏并移除该项 */
|
||
async function removeFavoriteFromList(job: JobItem, index: number) {
|
||
try {
|
||
const res = await removeJobFavorite(job.id)
|
||
if (res.code === '0') {
|
||
jobList.value.splice(index, 1)
|
||
total.value = Math.max(0, total.value - 1)
|
||
ElMessage.success('已取消收藏')
|
||
loadFavoriteCount()
|
||
}
|
||
} catch (e) {
|
||
console.error('取消收藏失败', e)
|
||
}
|
||
}
|
||
|
||
/** 打开不感兴趣弹窗 — 记录当前职位 ID 并重置表单 */
|
||
function openDislikeDialog(job: JobItem) {
|
||
dislikeJobId.value = job.id
|
||
dislikeDialogRef.value?.resetForm()
|
||
showDislikeDialog.value = true
|
||
}
|
||
|
||
/** 不感兴趣提交成功后,从列表中移除该职位 */
|
||
function removeDislikedJob() {
|
||
if (!dislikeJobId.value) return
|
||
const index = jobList.value.findIndex(j => j.id === dislikeJobId.value)
|
||
if (index !== -1) {
|
||
jobList.value.splice(index, 1)
|
||
total.value = Math.max(0, total.value - 1)
|
||
}
|
||
}
|
||
|
||
/** 打开问题反馈弹窗 — 记录当前职位 ID 并重置表单 */
|
||
function openFeedbackDialog(job: JobItem) {
|
||
feedbackJobId.value = job.id
|
||
feedbackDialogRef.value?.resetForm()
|
||
showFeedbackDialog.value = true
|
||
}
|
||
</script>
|