bug处理
This commit is contained in:
@@ -3,6 +3,16 @@ import aiService from '@/utils/aiRequest'
|
||||
import type { ApiResult } from '@/api/auth'
|
||||
import type { AiResult } from '@/utils/aiRequest'
|
||||
|
||||
// ==================== 岗位总数 ====================
|
||||
|
||||
/**
|
||||
* 获取全网实时岗位总数
|
||||
* GET /job/count
|
||||
*/
|
||||
export function fetchJobCount() {
|
||||
return request.get<any, ApiResult<number>>('/job/count')
|
||||
}
|
||||
|
||||
// ==================== 匹配详情 ====================
|
||||
|
||||
/** 匹配度详情 */
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* ==================== 岗位统计卡片 ==================== */
|
||||
.job-stats-cards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
margin-left: auto;
|
||||
filter: drop-shadow(0px 2px 10px rgba(0, 221, 179, 0.16));
|
||||
|
||||
/* 全网实时岗位卡片 */
|
||||
&__count-card {
|
||||
position: relative;
|
||||
height: 0.43rem;
|
||||
background: #0A171C;
|
||||
border: 1px solid #1C4C55;
|
||||
border-radius: 0.05rem;
|
||||
}
|
||||
|
||||
/* 状态指示圆点 */
|
||||
&__dot {
|
||||
//position: absolute;
|
||||
width: 0.08rem;
|
||||
height: 0.08rem;
|
||||
background: #2FE9F4;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 4px rgba(47, 233, 244, 0.75);
|
||||
}
|
||||
|
||||
/* 岗位数字 */
|
||||
&__count-num {
|
||||
|
||||
font-family: 'MiSans';
|
||||
font-weight: 630;
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.21rem;
|
||||
color: #F4FFFF;
|
||||
}
|
||||
|
||||
/* 岗位标签文字 */
|
||||
&__count-label {
|
||||
font-family: 'MiSans';
|
||||
font-weight: 330;
|
||||
font-size: 0.10rem;
|
||||
line-height: 0.13rem;
|
||||
color: #8AA8AF;
|
||||
}
|
||||
|
||||
/* 可投率卡片 */
|
||||
&__match-card {
|
||||
position: relative;
|
||||
width: 0.52rem;
|
||||
height: 0.42rem;
|
||||
background: #08161B;
|
||||
border: 1px solid #20626D;
|
||||
border-radius: 0.05rem;
|
||||
}
|
||||
|
||||
/* 可投率数字 */
|
||||
&__match-num {
|
||||
position: absolute;
|
||||
left: 0.08rem;
|
||||
top: 0.04rem;
|
||||
font-family: 'MiSans';
|
||||
font-weight: 630;
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.21rem;
|
||||
color: #2FE9F4;
|
||||
}
|
||||
|
||||
/* 可投率标签文字 */
|
||||
&__match-label {
|
||||
position: absolute;
|
||||
left: 0.08rem;
|
||||
top: 0.245rem;
|
||||
font-family: 'MiSans';
|
||||
font-weight: 520;
|
||||
font-size: 0.10rem;
|
||||
line-height: 0.13rem;
|
||||
color: #8FB7BE;
|
||||
}
|
||||
}
|
||||
|
||||
.job-page-header {
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
:categoryIds="step2CategoryIds"
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="true"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="prefFilterTriggerStyle"
|
||||
:displayStyle="prefFilterDisplayStyle"
|
||||
@update:categoryIds="onStep2CategoryChange"
|
||||
@@ -86,7 +87,8 @@
|
||||
:industryIds="step2IndustryIds"
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="true"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="prefFilterTriggerStyle"
|
||||
:displayStyle="prefFilterDisplayStyle"
|
||||
@update:industryIds="onStep2IndustryChange"
|
||||
|
||||
@@ -150,9 +150,9 @@ interface ChatMsg {
|
||||
|
||||
// ==================== 本地缓存 ====================
|
||||
|
||||
/** 获取当前用户的缓存 key */
|
||||
/** 获取当前用户的缓存 key,使用 vuex 中的用户 ID 区分不同账号 */
|
||||
function getCacheKey(): string {
|
||||
const userId = sessionStorage.getItem('userId') || 'anonymous'
|
||||
const userId = store.state.userInfo?.id || 'anonymous'
|
||||
return `nova_chat_${userId}`
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ async function sendMessage() {
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
// 从缓存加载聊天记录
|
||||
// 从缓存加载聊天记录(如果用户信息已就绪则按用户隔离)
|
||||
messages.value = loadChatFromCache()
|
||||
if (messages.value.length > 0) {
|
||||
scrollToBottom()
|
||||
@@ -283,6 +283,17 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 userInfo 变化:用户登录后 userInfo 从 null 变为有值时重新加载对应用户的缓存
|
||||
watch(() => store.state.userInfo, (newInfo, oldInfo) => {
|
||||
if (newInfo && newInfo.id && (!oldInfo || !oldInfo.id)) {
|
||||
// 用户信息就绪,重新加载该用户的聊天记录
|
||||
messages.value = loadChatFromCache()
|
||||
if (messages.value.length > 0) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 jobId 变化
|
||||
watch(() => props.jobId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId && !isJobDetailPage.value) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="selectorTriggerStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
@update:categoryIds="onCategoryChange"
|
||||
@@ -33,6 +34,7 @@
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="selectorTriggerStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
@update:industryIds="onIndustryChange"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-f mb16 prl24 ptb16">
|
||||
<div class="bg-f mb16 prl24 ptb16 dflex aliite-c">
|
||||
<div class="job-page-header dflex-start aliite-c">
|
||||
<h2 class="job-page-header__title">发现理想职位</h2>
|
||||
<div class="job-page-header__tabs ml30 ">
|
||||
@@ -17,6 +17,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 岗位统计卡片区域 -->
|
||||
<div class="job-stats-cards">
|
||||
<!-- 全网实时岗位卡片 -->
|
||||
<div class="job-stats-cards__count-card prl10 dflex-center aliite-c">
|
||||
<span class="job-stats-cards__dot"></span>
|
||||
<div class="ml8">
|
||||
<div class="job-stats-cards__count-num">{{ formattedJobCount }}</div>
|
||||
<div class="job-stats-cards__count-label">全网实时岗位</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可投率卡片 -->
|
||||
<div class="job-stats-cards__match-card">
|
||||
<span class="job-stats-cards__match-num">92%</span>
|
||||
<span class="job-stats-cards__match-label">可投率</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,7 +41,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { fetchFavoriteCount, fetchApplyCount } from '@/api/jobs'
|
||||
import { fetchFavoriteCount, fetchApplyCount, fetchJobCount } from '@/api/jobs'
|
||||
|
||||
// ==================== Props & Emits ====================
|
||||
|
||||
@@ -50,12 +66,32 @@ const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||
|
||||
// ==================== 收藏 & 投递统计 ====================
|
||||
|
||||
/** 全网岗位总数 */
|
||||
const jobCount = ref(0)
|
||||
|
||||
/** 格式化岗位总数(每3位加逗号) */
|
||||
const formattedJobCount = computed(() => {
|
||||
return jobCount.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
})
|
||||
|
||||
/** 收藏总数 */
|
||||
const favoriteTotal = ref(0)
|
||||
|
||||
/** 投递总数 */
|
||||
const applyTotal = ref(0)
|
||||
|
||||
/** 加载岗位总数 */
|
||||
async function loadJobCount() {
|
||||
try {
|
||||
const res = await fetchJobCount()
|
||||
if (res.code === '0' && res.data !== undefined) {
|
||||
jobCount.value = Number(res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载岗位总数失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载收藏统计 */
|
||||
async function loadFavoriteCount() {
|
||||
try {
|
||||
@@ -82,12 +118,20 @@ async function loadApplyCount() {
|
||||
|
||||
/** 组件挂载时请求收藏和投递统计 */
|
||||
onMounted(() => {
|
||||
// 岗位总数不需要登录态
|
||||
loadJobCount()
|
||||
if (isAuthenticated.value) {
|
||||
loadFavoriteCount()
|
||||
loadApplyCount()
|
||||
}
|
||||
})
|
||||
|
||||
/** 暴露刷新方法给父组件调用,以便收藏/投递操作后同步刷新计数 */
|
||||
defineExpose({
|
||||
loadFavoriteCount,
|
||||
loadApplyCount,
|
||||
})
|
||||
|
||||
// ==================== 常量数据 ====================
|
||||
|
||||
/** Tab 选项列表,count 用于显示圆形徽章计数 */
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
class="industry-selector__search-item"
|
||||
v-for="r in searchResults"
|
||||
:key="r.node.id"
|
||||
@click="toggleItem(r.node)"
|
||||
@click="handleSearchItemClick(r.node)"
|
||||
>
|
||||
<span>{{ r.path }}</span>
|
||||
<!-- 已选中项显示勾选图标 -->
|
||||
@@ -69,8 +69,8 @@
|
||||
<div class="industry-selector__search-empty">无匹配结果</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示:双击选中一级(仅 allowParentSelect 开启时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect" class="industry-selector__hint">(双击可选中一级行业分类)</div>
|
||||
<!-- 提示:双击选中一级(仅 allowParentSelect 开启且 allowParentSelectClick 关闭时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect && !allowParentSelectClick" class="industry-selector__hint">(双击可选中一级行业分类)</div>
|
||||
|
||||
<!-- 分栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
||||
<div v-if="searchText.length < 2" class="industry-selector__columns">
|
||||
@@ -80,14 +80,14 @@
|
||||
class="industry-selector__col-item"
|
||||
:class="{
|
||||
'industry-selector__col-item--active': level > 1 && activeParentId === parent.id,
|
||||
'industry-selector__col-item--selected': (level === 1 || allowParentSelect) && isSelected(parent.id)
|
||||
'industry-selector__col-item--selected': (level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(parent.id)
|
||||
}"
|
||||
v-for="parent in industries"
|
||||
:key="parent.id"
|
||||
@click="selectParent(parent.id)"
|
||||
>
|
||||
{{ parent.name }}
|
||||
<svg v-if="(level === 1 || allowParentSelect) && isSelected(parent.id)" class="industry-selector__check" viewBox="0 0 12 12">
|
||||
<svg v-if="(level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(parent.id)" class="industry-selector__check" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@
|
||||
:class="{ 'industry-selector__col-item--selected': isSelected(child.id) }"
|
||||
v-for="child in activeChildren"
|
||||
:key="child.id"
|
||||
@click="toggleItem({ id: child.id, name: child.name, level: child.level })"
|
||||
@click="selectChild(child)"
|
||||
>
|
||||
{{ child.name }}
|
||||
<svg v-if="isSelected(child.id)" class="industry-selector__check" viewBox="0 0 12 12">
|
||||
@@ -144,6 +144,8 @@ const props = withDefaults(
|
||||
maxSelect?: number
|
||||
/** 是否允许双击选中一级行业 */
|
||||
allowParentSelect?: boolean
|
||||
/** 是否允许单击选中父级(选子级时自动移除已选的父级) */
|
||||
allowParentSelectClick?: boolean
|
||||
/** 展示/选择到第几级:1=只选一级,2=选到二级 */
|
||||
level?: 1 | 2
|
||||
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
|
||||
@@ -156,6 +158,7 @@ const props = withDefaults(
|
||||
{
|
||||
maxSelect: 3,
|
||||
allowParentSelect: false,
|
||||
allowParentSelectClick: false,
|
||||
level: 2,
|
||||
}
|
||||
)
|
||||
@@ -212,14 +215,16 @@ const activeChildren = computed<IndustryChild[]>(() => {
|
||||
return parent ? parent.children : []
|
||||
})
|
||||
|
||||
/** 搜索结果:根据 level 和 allowParentSelect 匹配对应级别 */
|
||||
/** 搜索结果:根据 level 和 allowParentSelect/allowParentSelectClick 匹配对应级别 */
|
||||
const searchResults = computed(() => {
|
||||
if (searchText.value.length < 2) return []
|
||||
const keyword = searchText.value.toLowerCase()
|
||||
const results: { path: string; node: SelectedNode }[] = []
|
||||
/** 是否允许选中父级 */
|
||||
const canSelectParent = props.allowParentSelect || props.allowParentSelectClick
|
||||
for (const parent of industries.value) {
|
||||
// 一级:level=1 时作为末级可搜索,或 allowParentSelect 且 level=2 时可搜索
|
||||
if ((props.level === 1 || props.allowParentSelect) && parent.name.toLowerCase().includes(keyword)) {
|
||||
// 一级:level=1 时作为末级可搜索,或允许选中父级且 level=2 时可搜索
|
||||
if ((props.level === 1 || canSelectParent) && parent.name.toLowerCase().includes(keyword)) {
|
||||
results.push({ path: parent.name, node: { id: parent.id, name: parent.name, level: parent.level } })
|
||||
}
|
||||
if (props.level === 2) {
|
||||
@@ -248,6 +253,13 @@ function selectParent(id: string) {
|
||||
if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level })
|
||||
return
|
||||
}
|
||||
// allowParentSelectClick 模式:单击直接选中一级,同时展开子级
|
||||
if (props.allowParentSelectClick) {
|
||||
const l1 = industries.value.find(c => c.id === id)
|
||||
if (l1) toggleItemWithHierarchy({ id: l1.id, name: l1.name, level: l1.level })
|
||||
activeParentId.value = id
|
||||
return
|
||||
}
|
||||
if (props.allowParentSelect) {
|
||||
const now = Date.now()
|
||||
const lastTime = level1LastClickTime.value[id] || 0
|
||||
@@ -279,6 +291,79 @@ function toggleItem(node: SelectedNode) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allowParentSelectClick 模式下的选中/取消逻辑:
|
||||
* - 选中子级时,移除已选中的父级
|
||||
* - 选中父级时,移除已选中的该父级下的所有子级
|
||||
*/
|
||||
function toggleItemWithHierarchy(node: SelectedNode) {
|
||||
const idx = selectedItems.value.findIndex(i => i.id === node.id)
|
||||
if (idx >= 0) {
|
||||
// 取消选中
|
||||
selectedItems.value.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
// 获取祖先和后代 ID
|
||||
const ancestorIds = getAncestorIds(node.id)
|
||||
const descendantIds = getDescendantIds(node.id)
|
||||
// 移除已选中的祖先和后代
|
||||
selectedItems.value = selectedItems.value.filter(
|
||||
i => !ancestorIds.has(i.id) && !descendantIds.has(i.id)
|
||||
)
|
||||
// 检查数量限制
|
||||
if (selectedItems.value.length >= props.maxSelect) {
|
||||
ElMessage.warning(`最多只能选择${props.maxSelect}个行业`)
|
||||
return
|
||||
}
|
||||
selectedItems.value.push({ ...node })
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有祖先 ID(向上查找) */
|
||||
function getAncestorIds(nodeId: string): Set<string> {
|
||||
const ancestors = new Set<string>()
|
||||
for (const parent of industries.value) {
|
||||
for (const child of parent.children) {
|
||||
if (child.id === nodeId) {
|
||||
ancestors.add(parent.id)
|
||||
return ancestors
|
||||
}
|
||||
}
|
||||
}
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有后代 ID(向下查找) */
|
||||
function getDescendantIds(nodeId: string): Set<string> {
|
||||
const descendants = new Set<string>()
|
||||
for (const parent of industries.value) {
|
||||
if (parent.id === nodeId) {
|
||||
for (const child of parent.children) {
|
||||
descendants.add(child.id)
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
|
||||
/** 点击右栏二级行业 */
|
||||
function selectChild(child: { id: string; name: string; level: number }) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy({ id: child.id, name: child.name, level: child.level })
|
||||
} else {
|
||||
toggleItem({ id: child.id, name: child.name, level: child.level })
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索结果项点击处理 */
|
||||
function handleSearchItemClick(node: SelectedNode) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy(node)
|
||||
} else {
|
||||
toggleItem(node)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从选中区移除指定行业 */
|
||||
function removeItem(item: SelectedNode) {
|
||||
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
class="job-category-selector__search-item"
|
||||
v-for="r in searchResults"
|
||||
:key="r.node.id"
|
||||
@click="toggleItem(r.node)"
|
||||
@click="handleSearchItemClick(r.node)"
|
||||
>
|
||||
<span>{{ r.path }}</span>
|
||||
<svg v-if="isSelected(r.node.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
@@ -71,8 +71,8 @@
|
||||
<div class="job-category-selector__search-empty">无匹配结果</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示:双击选中一二级(仅 allowParentSelect 开启时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect" class="job-category-selector__hint">(双击可选中一级/二级岗位分类)</div>
|
||||
<!-- 提示:双击选中一二级(仅 allowParentSelect 开启且 allowParentSelectClick 关闭时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect && !allowParentSelectClick" class="job-category-selector__hint">(双击可选中一级/二级岗位分类)</div>
|
||||
|
||||
<!-- 三栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
||||
<div v-if="searchText.length < 2" class="job-category-selector__columns">
|
||||
@@ -82,14 +82,14 @@
|
||||
class="job-category-selector__col-item"
|
||||
:class="{
|
||||
'job-category-selector__col-item--active': level > 1 && activeLevel1Id === item.id,
|
||||
'job-category-selector__col-item--selected': (level === 1 || allowParentSelect) && isSelected(item.id)
|
||||
'job-category-selector__col-item--selected': (level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)
|
||||
}"
|
||||
v-for="item in categories"
|
||||
:key="item.id"
|
||||
@click="selectLevel1(item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<svg v-if="(level === 1 || allowParentSelect) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<svg v-if="(level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -101,14 +101,14 @@
|
||||
class="job-category-selector__col-item"
|
||||
:class="{
|
||||
'job-category-selector__col-item--active': level > 2 && activeLevel2Id === item.id,
|
||||
'job-category-selector__col-item--selected': (level === 2 || allowParentSelect) && isSelected(item.id)
|
||||
'job-category-selector__col-item--selected': (level === 2 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)
|
||||
}"
|
||||
v-for="item in level2List"
|
||||
:key="item.id"
|
||||
@click="selectLevel2(item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<svg v-if="(level === 2 || allowParentSelect) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<svg v-if="(level === 2 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@
|
||||
:class="{ 'job-category-selector__col-item--selected': isSelected(item.id) }"
|
||||
v-for="item in level3List"
|
||||
:key="item.id"
|
||||
@click="toggleItem({ id: item.id, name: item.name, level: item.level })"
|
||||
@click="selectLevel3(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<svg v-if="isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
@@ -167,6 +167,8 @@ const props = withDefaults(
|
||||
maxSelect?: number
|
||||
/** 是否允许双击选中一二级分类 */
|
||||
allowParentSelect?: boolean
|
||||
/** 是否允许单击选中父级(选子级时自动移除已选的父级) */
|
||||
allowParentSelectClick?: boolean
|
||||
/** 展示/选择到第几级:1=只选一级,2=选到二级,3=选到三级 */
|
||||
level?: 1 | 2 | 3
|
||||
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
|
||||
@@ -179,6 +181,7 @@ const props = withDefaults(
|
||||
{
|
||||
maxSelect: 3,
|
||||
allowParentSelect: false,
|
||||
allowParentSelectClick: false,
|
||||
level: 3,
|
||||
}
|
||||
)
|
||||
@@ -248,20 +251,22 @@ const level3List = computed<JobCategoryLeaf[]>(() => {
|
||||
return mid ? mid.children : []
|
||||
})
|
||||
|
||||
/** 搜索结果:根据 level 和 allowParentSelect 匹配对应级别 */
|
||||
/** 搜索结果:根据 level 和 allowParentSelect/allowParentSelectClick 匹配对应级别 */
|
||||
const searchResults = computed(() => {
|
||||
if (searchText.value.length < 2) return []
|
||||
const keyword = searchText.value.toLowerCase()
|
||||
const results: { path: string; node: SelectedNode }[] = []
|
||||
/** 是否允许选中父级(双击或单击模式均可) */
|
||||
const canSelectParent = props.allowParentSelect || props.allowParentSelectClick
|
||||
for (const l1 of categories.value) {
|
||||
// 一级:level=1 时作为末级可搜索,或 allowParentSelect 且 level>1 时可搜索
|
||||
if ((props.level === 1 || props.allowParentSelect) && l1.name.toLowerCase().includes(keyword)) {
|
||||
// 一级:level=1 时作为末级可搜索,或允许选中父级且 level>1 时可搜索
|
||||
if ((props.level === 1 || canSelectParent) && l1.name.toLowerCase().includes(keyword)) {
|
||||
results.push({ path: l1.name, node: { id: l1.id, name: l1.name, level: l1.level } })
|
||||
}
|
||||
if (props.level >= 2) {
|
||||
for (const l2 of l1.children) {
|
||||
// 二级:level=2 时作为末级可搜索,或 allowParentSelect 且 level=3 时可搜索
|
||||
if ((props.level === 2 || props.allowParentSelect) && l2.name.toLowerCase().includes(keyword)) {
|
||||
// 二级:level=2 时作为末级可搜索,或允许选中父级且 level=3 时可搜索
|
||||
if ((props.level === 2 || canSelectParent) && l2.name.toLowerCase().includes(keyword)) {
|
||||
results.push({ path: `${l1.name} → ${l2.name}`, node: { id: l2.id, name: l2.name, level: l2.level } })
|
||||
}
|
||||
if (props.level === 3) {
|
||||
@@ -292,6 +297,14 @@ function selectLevel1(id: string) {
|
||||
if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level })
|
||||
return
|
||||
}
|
||||
// allowParentSelectClick 模式:单击直接选中一级,同时展开子级
|
||||
if (props.allowParentSelectClick) {
|
||||
const l1 = categories.value.find(c => c.id === id)
|
||||
if (l1) toggleItemWithHierarchy({ id: l1.id, name: l1.name, level: l1.level })
|
||||
activeLevel1Id.value = id
|
||||
activeLevel2Id.value = ''
|
||||
return
|
||||
}
|
||||
if (props.allowParentSelect) {
|
||||
const now = Date.now()
|
||||
const lastTime = level1LastClickTime.value[id] || 0
|
||||
@@ -319,6 +332,13 @@ function selectLevel2(id: string) {
|
||||
if (l2) toggleItem({ id: l2.id, name: l2.name, level: l2.level })
|
||||
return
|
||||
}
|
||||
// allowParentSelectClick 模式:单击直接选中二级,同时展开子级
|
||||
if (props.allowParentSelectClick) {
|
||||
const l2 = level2List.value.find(c => c.id === id)
|
||||
if (l2) toggleItemWithHierarchy({ id: l2.id, name: l2.name, level: l2.level })
|
||||
activeLevel2Id.value = id
|
||||
return
|
||||
}
|
||||
if (props.allowParentSelect) {
|
||||
const now = Date.now()
|
||||
const lastTime = level2LastClickTime.value[id] || 0
|
||||
@@ -350,6 +370,100 @@ function toggleItem(node: SelectedNode) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allowParentSelectClick 模式下的选中/取消逻辑:
|
||||
* - 选中子级时,移除已选中的父级(因为选了更具体的)
|
||||
* - 选中父级时,移除已选中的该父级下的所有子级
|
||||
*/
|
||||
function toggleItemWithHierarchy(node: SelectedNode) {
|
||||
const idx = selectedItems.value.findIndex(i => i.id === node.id)
|
||||
if (idx >= 0) {
|
||||
// 取消选中
|
||||
selectedItems.value.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
// 选中前先做层级互斥处理
|
||||
// 获取当前节点的祖先 ID 列表和后代 ID 列表
|
||||
const ancestorIds = getAncestorIds(node.id)
|
||||
const descendantIds = getDescendantIds(node.id)
|
||||
// 移除已选中的祖先和后代
|
||||
selectedItems.value = selectedItems.value.filter(
|
||||
i => !ancestorIds.has(i.id) && !descendantIds.has(i.id)
|
||||
)
|
||||
// 检查数量限制
|
||||
if (selectedItems.value.length >= props.maxSelect) {
|
||||
ElMessage.warning(`最多只能选择${props.maxSelect}个岗位`)
|
||||
return
|
||||
}
|
||||
selectedItems.value.push({ ...node })
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有祖先 ID(向上查找) */
|
||||
function getAncestorIds(nodeId: string): Set<string> {
|
||||
const ancestors = new Set<string>()
|
||||
for (const l1 of categories.value) {
|
||||
for (const l2 of l1.children) {
|
||||
for (const l3 of l2.children) {
|
||||
if (l3.id === nodeId) {
|
||||
ancestors.add(l2.id)
|
||||
ancestors.add(l1.id)
|
||||
return ancestors
|
||||
}
|
||||
}
|
||||
if (l2.id === nodeId) {
|
||||
ancestors.add(l1.id)
|
||||
return ancestors
|
||||
}
|
||||
}
|
||||
}
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有后代 ID(向下查找) */
|
||||
function getDescendantIds(nodeId: string): Set<string> {
|
||||
const descendants = new Set<string>()
|
||||
for (const l1 of categories.value) {
|
||||
if (l1.id === nodeId) {
|
||||
// 是一级,收集所有二级和三级
|
||||
for (const l2 of l1.children) {
|
||||
descendants.add(l2.id)
|
||||
for (const l3 of l2.children) {
|
||||
descendants.add(l3.id)
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
for (const l2 of l1.children) {
|
||||
if (l2.id === nodeId) {
|
||||
// 是二级,收集所有三级
|
||||
for (const l3 of l2.children) {
|
||||
descendants.add(l3.id)
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
|
||||
/** 点击右栏三级岗位 */
|
||||
function selectLevel3(item: { id: string; name: string; level: number }) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy({ id: item.id, name: item.name, level: item.level })
|
||||
} else {
|
||||
toggleItem({ id: item.id, name: item.name, level: item.level })
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索结果项点击处理 */
|
||||
function handleSearchItemClick(node: SelectedNode) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy(node)
|
||||
} else {
|
||||
toggleItem(node)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从选中区移除指定岗位 */
|
||||
function removeItem(item: SelectedNode) {
|
||||
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
|
||||
|
||||
@@ -409,6 +409,7 @@
|
||||
:level="2"
|
||||
:maxSelect="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:industryIds="onHomeIndustryChange"
|
||||
@@ -428,6 +429,7 @@
|
||||
:level="3"
|
||||
:maxSelect="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:categoryIds="onHomeCategoryChange"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<SideNav />
|
||||
<div class="job-detail__content">
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader :activeTab="cachedTab" />
|
||||
<JobPageHeader ref="jobPageHeaderRef" :activeTab="cachedTab" />
|
||||
<!-- 主内容卡片 -->
|
||||
<div class="job-detail__main-card">
|
||||
<!-- 返回按钮 -->
|
||||
@@ -443,6 +443,9 @@ function goBack() {
|
||||
/** AiChat 组件引用 */
|
||||
const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null)
|
||||
|
||||
/** JobPageHeader 组件引用,用于刷新收藏/投递计数 */
|
||||
const jobPageHeaderRef = ref<InstanceType<typeof JobPageHeader> | null>(null)
|
||||
|
||||
/** 问 Nova — 触发 AI 聊天,推送提示消息 */
|
||||
function handleAskNova() {
|
||||
aiChatRef.value?.triggerAsk()
|
||||
@@ -556,6 +559,8 @@ async function toggleFavorite() {
|
||||
const wasFavorite = job.isFavorite
|
||||
job.isFavorite = !job.isFavorite
|
||||
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
|
||||
// 同步刷新 JobPageHeader 组件内的收藏计数
|
||||
jobPageHeaderRef.value?.loadFavoriteCount()
|
||||
// 同步收藏状态到 Jobs 页面缓存,返回时无需刷新列表即可看到最新状态
|
||||
const cache = store.state.jobListCache
|
||||
if (cache) {
|
||||
|
||||
+13
-4
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader v-model:activeTab="activeTab" />
|
||||
<JobPageHeader ref="jobPageHeaderRef" v-model:activeTab="activeTab" />
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<div v-if="isRecommendTab" class="jobs-page__filters-bar" :style="showAiRecommendTip?'':'margin-bottom:0.2rem'">
|
||||
@@ -42,7 +42,8 @@
|
||||
:categoryIds="selectedCategoryIds"
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="true"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="selectorTriggerStyle"
|
||||
:hoverStyle="selectorHoverStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
@@ -55,7 +56,8 @@
|
||||
:industryIds="selectedIndustryIds"
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="true"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="selectorTriggerStyle"
|
||||
:hoverStyle="selectorHoverStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
@@ -655,7 +657,7 @@ const aiCategoryNames = computed<string>(() => {
|
||||
|
||||
/** 是否显示 AI 推荐提示条 */
|
||||
const showAiRecommendTip = computed(() => {
|
||||
return isRecommendTab.value && (aiIndustryNames.value !== null || aiCategoryNames.value !== null)
|
||||
return isRecommendTab.value && (aiIndustryNames.value || aiCategoryNames.value)
|
||||
})
|
||||
|
||||
/** 行业选择变更回调 */
|
||||
@@ -1459,6 +1461,9 @@ function goToDetail(job: JobItem) {
|
||||
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
|
||||
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
|
||||
|
||||
/** JobPageHeader 组件引用,用于刷新收藏/投递计数 */
|
||||
const jobPageHeaderRef = ref<InstanceType<typeof JobPageHeader> | null>(null)
|
||||
|
||||
/** 收藏/取消收藏岗位 */
|
||||
async function toggleFavorite(job: JobItem) {
|
||||
try {
|
||||
@@ -1471,6 +1476,8 @@ async function toggleFavorite(job: JobItem) {
|
||||
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
|
||||
// 刷新收藏统计
|
||||
loadFavoriteCount()
|
||||
// 同步刷新 JobPageHeader 组件内的收藏计数
|
||||
jobPageHeaderRef.value?.loadFavoriteCount()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('收藏操作失败', e)
|
||||
@@ -1486,6 +1493,8 @@ async function removeFavoriteFromList(job: JobItem, index: number) {
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
ElMessage.success('已取消收藏')
|
||||
loadFavoriteCount()
|
||||
// 同步刷新 JobPageHeader 组件内的收藏计数
|
||||
jobPageHeaderRef.value?.loadFavoriteCount()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('取消收藏失败', e)
|
||||
|
||||
+4
-2
@@ -126,7 +126,7 @@
|
||||
<input ref="fileInputRef" type="file" accept=".pdf,.doc,.docx" class="login-view__file-input" @change="handleFileChange" />
|
||||
<!-- 上传按钮 -->
|
||||
<button class="login-view__upload-btn" @click="triggerFileInput">+ 上传简历</button>
|
||||
<div class="tac"><span class="color-middle cursor-po fs14" @click="handleEnterDashboard">跳过,稍后填写</span></div>
|
||||
<!-- <div class="tac"><span class="color-middle cursor-po fs14" @click="handleEnterDashboard">跳过,稍后填写</span></div>-->
|
||||
</template>
|
||||
|
||||
<!-- 状态2:解析中 parsing -->
|
||||
@@ -210,7 +210,9 @@
|
||||
<template v-if="resumeState === 'failed'">
|
||||
<div class="login-view__heading m0auto">
|
||||
<h2 class="login-view__title">简历上传失败</h2>
|
||||
<p class="login-view__subtitle">文件没有解析成功,请检查格式后重新上传 或 <span class="color-accent cursor-po" @click="handleEnterDashboard">跳过,稍后填写</span></p>
|
||||
<p class="login-view__subtitle">文件没有解析成功,请检查格式后重新上传
|
||||
<!-- 或 <span class="color-accent cursor-po" @click="handleEnterDashboard">跳过,稍后填写</span>-->
|
||||
</p>
|
||||
</div>
|
||||
<!-- 失败面板 -->
|
||||
<div class="login-view__failed-panel">
|
||||
|
||||
Reference in New Issue
Block a user