bug处理

This commit is contained in:
2026-07-13 23:01:49 +08:00
parent aed2535ab2
commit 7abe158dbb
12 changed files with 403 additions and 36 deletions
+10
View File
@@ -3,6 +3,16 @@ import aiService from '@/utils/aiRequest'
import type { ApiResult } from '@/api/auth' import type { ApiResult } from '@/api/auth'
import type { AiResult } from '@/utils/aiRequest' 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 *; @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 { .job-page-header {
+4 -2
View File
@@ -76,7 +76,8 @@
:categoryIds="step2CategoryIds" :categoryIds="step2CategoryIds"
:maxSelect="3" :maxSelect="3"
:level="3" :level="3"
:allowParentSelect="true" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="prefFilterTriggerStyle" :triggerStyle="prefFilterTriggerStyle"
:displayStyle="prefFilterDisplayStyle" :displayStyle="prefFilterDisplayStyle"
@update:categoryIds="onStep2CategoryChange" @update:categoryIds="onStep2CategoryChange"
@@ -86,7 +87,8 @@
:industryIds="step2IndustryIds" :industryIds="step2IndustryIds"
:maxSelect="3" :maxSelect="3"
:level="2" :level="2"
:allowParentSelect="true" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="prefFilterTriggerStyle" :triggerStyle="prefFilterTriggerStyle"
:displayStyle="prefFilterDisplayStyle" :displayStyle="prefFilterDisplayStyle"
@update:industryIds="onStep2IndustryChange" @update:industryIds="onStep2IndustryChange"
+14 -3
View File
@@ -150,9 +150,9 @@ interface ChatMsg {
// ==================== 本地缓存 ==================== // ==================== 本地缓存 ====================
/** 获取当前用户的缓存 key */ /** 获取当前用户的缓存 key,使用 vuex 中的用户 ID 区分不同账号 */
function getCacheKey(): string { function getCacheKey(): string {
const userId = sessionStorage.getItem('userId') || 'anonymous' const userId = store.state.userInfo?.id || 'anonymous'
return `nova_chat_${userId}` return `nova_chat_${userId}`
} }
@@ -272,7 +272,7 @@ async function sendMessage() {
// ==================== 生命周期 ==================== // ==================== 生命周期 ====================
onMounted(() => { onMounted(() => {
// 从缓存加载聊天记录 // 从缓存加载聊天记录(如果用户信息已就绪则按用户隔离)
messages.value = loadChatFromCache() messages.value = loadChatFromCache()
if (messages.value.length > 0) { if (messages.value.length > 0) {
scrollToBottom() 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 变化 // 监听 jobId 变化
watch(() => props.jobId, (newId, oldId) => { watch(() => props.jobId, (newId, oldId) => {
if (newId && newId !== oldId && !isJobDetailPage.value) { if (newId && newId !== oldId && !isJobDetailPage.value) {
+2
View File
@@ -19,6 +19,7 @@
:maxSelect="3" :maxSelect="3"
:level="3" :level="3"
:allowParentSelect="false" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="selectorTriggerStyle" :triggerStyle="selectorTriggerStyle"
:displayStyle="selectorDisplayStyle" :displayStyle="selectorDisplayStyle"
@update:categoryIds="onCategoryChange" @update:categoryIds="onCategoryChange"
@@ -33,6 +34,7 @@
:maxSelect="3" :maxSelect="3"
:level="2" :level="2"
:allowParentSelect="false" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="selectorTriggerStyle" :triggerStyle="selectorTriggerStyle"
:displayStyle="selectorDisplayStyle" :displayStyle="selectorDisplayStyle"
@update:industryIds="onIndustryChange" @update:industryIds="onIndustryChange"
+46 -2
View File
@@ -1,5 +1,5 @@
<template> <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"> <div class="job-page-header dflex-start aliite-c">
<h2 class="job-page-header__title">发现理想职位</h2> <h2 class="job-page-header__title">发现理想职位</h2>
<div class="job-page-header__tabs ml30 "> <div class="job-page-header__tabs ml30 ">
@@ -17,6 +17,22 @@
</div> </div>
</div> </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> </div>
</template> </template>
@@ -25,7 +41,7 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { fetchFavoriteCount, fetchApplyCount } from '@/api/jobs' import { fetchFavoriteCount, fetchApplyCount, fetchJobCount } from '@/api/jobs'
// ==================== Props & Emits ==================== // ==================== 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 favoriteTotal = ref(0)
/** 投递总数 */ /** 投递总数 */
const applyTotal = 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() { async function loadFavoriteCount() {
try { try {
@@ -82,12 +118,20 @@ async function loadApplyCount() {
/** 组件挂载时请求收藏和投递统计 */ /** 组件挂载时请求收藏和投递统计 */
onMounted(() => { onMounted(() => {
// 岗位总数不需要登录态
loadJobCount()
if (isAuthenticated.value) { if (isAuthenticated.value) {
loadFavoriteCount() loadFavoriteCount()
loadApplyCount() loadApplyCount()
} }
}) })
/** 暴露刷新方法给父组件调用,以便收藏/投递操作后同步刷新计数 */
defineExpose({
loadFavoriteCount,
loadApplyCount,
})
// ==================== 常量数据 ==================== // ==================== 常量数据 ====================
/** Tab 选项列表,count 用于显示圆形徽章计数 */ /** Tab 选项列表,count 用于显示圆形徽章计数 */
+94 -9
View File
@@ -54,7 +54,7 @@
class="industry-selector__search-item" class="industry-selector__search-item"
v-for="r in searchResults" v-for="r in searchResults"
:key="r.node.id" :key="r.node.id"
@click="toggleItem(r.node)" @click="handleSearchItemClick(r.node)"
> >
<span>{{ r.path }}</span> <span>{{ r.path }}</span>
<!-- 已选中项显示勾选图标 --> <!-- 已选中项显示勾选图标 -->
@@ -69,8 +69,8 @@
<div class="industry-selector__search-empty">无匹配结果</div> <div class="industry-selector__search-empty">无匹配结果</div>
</div> </div>
<!-- 提示双击选中一级 allowParentSelect 开启时显示 --> <!-- 提示双击选中一级 allowParentSelect 开启 allowParentSelectClick 关闭时显示 -->
<div v-if="searchText.length < 2 && allowParentSelect" class="industry-selector__hint">双击可选中一级行业分类</div> <div v-if="searchText.length < 2 && allowParentSelect && !allowParentSelectClick" class="industry-selector__hint">双击可选中一级行业分类</div>
<!-- 分栏联动选择区搜索关键词不足 2 字符时显示 --> <!-- 分栏联动选择区搜索关键词不足 2 字符时显示 -->
<div v-if="searchText.length < 2" class="industry-selector__columns"> <div v-if="searchText.length < 2" class="industry-selector__columns">
@@ -80,14 +80,14 @@
class="industry-selector__col-item" class="industry-selector__col-item"
:class="{ :class="{
'industry-selector__col-item--active': level > 1 && activeParentId === parent.id, '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" v-for="parent in industries"
:key="parent.id" :key="parent.id"
@click="selectParent(parent.id)" @click="selectParent(parent.id)"
> >
{{ parent.name }} {{ 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"/> <path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg> </svg>
</div> </div>
@@ -100,7 +100,7 @@
:class="{ 'industry-selector__col-item--selected': isSelected(child.id) }" :class="{ 'industry-selector__col-item--selected': isSelected(child.id) }"
v-for="child in activeChildren" v-for="child in activeChildren"
:key="child.id" :key="child.id"
@click="toggleItem({ id: child.id, name: child.name, level: child.level })" @click="selectChild(child)"
> >
{{ child.name }} {{ child.name }}
<svg v-if="isSelected(child.id)" class="industry-selector__check" viewBox="0 0 12 12"> <svg v-if="isSelected(child.id)" class="industry-selector__check" viewBox="0 0 12 12">
@@ -144,6 +144,8 @@ const props = withDefaults(
maxSelect?: number maxSelect?: number
/** 是否允许双击选中一级行业 */ /** 是否允许双击选中一级行业 */
allowParentSelect?: boolean allowParentSelect?: boolean
/** 是否允许单击选中父级(选子级时自动移除已选的父级) */
allowParentSelectClick?: boolean
/** 展示/选择到第几级:1=只选一级,2=选到二级 */ /** 展示/选择到第几级:1=只选一级,2=选到二级 */
level?: 1 | 2 level?: 1 | 2
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */ /** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
@@ -156,6 +158,7 @@ const props = withDefaults(
{ {
maxSelect: 3, maxSelect: 3,
allowParentSelect: false, allowParentSelect: false,
allowParentSelectClick: false,
level: 2, level: 2,
} }
) )
@@ -212,14 +215,16 @@ const activeChildren = computed<IndustryChild[]>(() => {
return parent ? parent.children : [] return parent ? parent.children : []
}) })
/** 搜索结果:根据 level 和 allowParentSelect 匹配对应级别 */ /** 搜索结果:根据 level 和 allowParentSelect/allowParentSelectClick 匹配对应级别 */
const searchResults = computed(() => { const searchResults = computed(() => {
if (searchText.value.length < 2) return [] if (searchText.value.length < 2) return []
const keyword = searchText.value.toLowerCase() const keyword = searchText.value.toLowerCase()
const results: { path: string; node: SelectedNode }[] = [] const results: { path: string; node: SelectedNode }[] = []
/** 是否允许选中父级 */
const canSelectParent = props.allowParentSelect || props.allowParentSelectClick
for (const parent of industries.value) { for (const parent of industries.value) {
// 一级:level=1 时作为末级可搜索,或 allowParentSelect 且 level=2 时可搜索 // 一级:level=1 时作为末级可搜索,或允许选中父级且 level=2 时可搜索
if ((props.level === 1 || props.allowParentSelect) && parent.name.toLowerCase().includes(keyword)) { 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 } }) results.push({ path: parent.name, node: { id: parent.id, name: parent.name, level: parent.level } })
} }
if (props.level === 2) { if (props.level === 2) {
@@ -248,6 +253,13 @@ function selectParent(id: string) {
if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level }) if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level })
return 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) { if (props.allowParentSelect) {
const now = Date.now() const now = Date.now()
const lastTime = level1LastClickTime.value[id] || 0 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) { function removeItem(item: SelectedNode) {
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id) selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
+127 -13
View File
@@ -57,7 +57,7 @@
class="job-category-selector__search-item" class="job-category-selector__search-item"
v-for="r in searchResults" v-for="r in searchResults"
:key="r.node.id" :key="r.node.id"
@click="toggleItem(r.node)" @click="handleSearchItemClick(r.node)"
> >
<span>{{ r.path }}</span> <span>{{ r.path }}</span>
<svg v-if="isSelected(r.node.id)" class="job-category-selector__check" viewBox="0 0 12 12"> <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 class="job-category-selector__search-empty">无匹配结果</div>
</div> </div>
<!-- 提示双击选中一二级 allowParentSelect 开启时显示 --> <!-- 提示双击选中一二级 allowParentSelect 开启 allowParentSelectClick 关闭时显示 -->
<div v-if="searchText.length < 2 && allowParentSelect" class="job-category-selector__hint">双击可选中一级/二级岗位分类</div> <div v-if="searchText.length < 2 && allowParentSelect && !allowParentSelectClick" class="job-category-selector__hint">双击可选中一级/二级岗位分类</div>
<!-- 三栏联动选择区搜索关键词不足 2 字符时显示 --> <!-- 三栏联动选择区搜索关键词不足 2 字符时显示 -->
<div v-if="searchText.length < 2" class="job-category-selector__columns"> <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"
:class="{ :class="{
'job-category-selector__col-item--active': level > 1 && activeLevel1Id === item.id, '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" v-for="item in categories"
:key="item.id" :key="item.id"
@click="selectLevel1(item.id)" @click="selectLevel1(item.id)"
> >
{{ item.name }} {{ 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"/> <path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg> </svg>
</div> </div>
@@ -101,14 +101,14 @@
class="job-category-selector__col-item" class="job-category-selector__col-item"
:class="{ :class="{
'job-category-selector__col-item--active': level > 2 && activeLevel2Id === item.id, '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" v-for="item in level2List"
:key="item.id" :key="item.id"
@click="selectLevel2(item.id)" @click="selectLevel2(item.id)"
> >
{{ item.name }} {{ 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"/> <path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg> </svg>
</div> </div>
@@ -123,7 +123,7 @@
:class="{ 'job-category-selector__col-item--selected': isSelected(item.id) }" :class="{ 'job-category-selector__col-item--selected': isSelected(item.id) }"
v-for="item in level3List" v-for="item in level3List"
:key="item.id" :key="item.id"
@click="toggleItem({ id: item.id, name: item.name, level: item.level })" @click="selectLevel3(item)"
> >
{{ item.name }} {{ item.name }}
<svg v-if="isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12"> <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 maxSelect?: number
/** 是否允许双击选中一二级分类 */ /** 是否允许双击选中一二级分类 */
allowParentSelect?: boolean allowParentSelect?: boolean
/** 是否允许单击选中父级(选子级时自动移除已选的父级) */
allowParentSelectClick?: boolean
/** 展示/选择到第几级:1=只选一级,2=选到二级,3=选到三级 */ /** 展示/选择到第几级:1=只选一级,2=选到二级,3=选到三级 */
level?: 1 | 2 | 3 level?: 1 | 2 | 3
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */ /** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
@@ -179,6 +181,7 @@ const props = withDefaults(
{ {
maxSelect: 3, maxSelect: 3,
allowParentSelect: false, allowParentSelect: false,
allowParentSelectClick: false,
level: 3, level: 3,
} }
) )
@@ -248,20 +251,22 @@ const level3List = computed<JobCategoryLeaf[]>(() => {
return mid ? mid.children : [] return mid ? mid.children : []
}) })
/** 搜索结果:根据 level 和 allowParentSelect 匹配对应级别 */ /** 搜索结果:根据 level 和 allowParentSelect/allowParentSelectClick 匹配对应级别 */
const searchResults = computed(() => { const searchResults = computed(() => {
if (searchText.value.length < 2) return [] if (searchText.value.length < 2) return []
const keyword = searchText.value.toLowerCase() const keyword = searchText.value.toLowerCase()
const results: { path: string; node: SelectedNode }[] = [] const results: { path: string; node: SelectedNode }[] = []
/** 是否允许选中父级(双击或单击模式均可) */
const canSelectParent = props.allowParentSelect || props.allowParentSelectClick
for (const l1 of categories.value) { for (const l1 of categories.value) {
// 一级:level=1 时作为末级可搜索,或 allowParentSelect 且 level>1 时可搜索 // 一级:level=1 时作为末级可搜索,或允许选中父级且 level>1 时可搜索
if ((props.level === 1 || props.allowParentSelect) && l1.name.toLowerCase().includes(keyword)) { 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 } }) results.push({ path: l1.name, node: { id: l1.id, name: l1.name, level: l1.level } })
} }
if (props.level >= 2) { if (props.level >= 2) {
for (const l2 of l1.children) { for (const l2 of l1.children) {
// 二级:level=2 时作为末级可搜索,或 allowParentSelect 且 level=3 时可搜索 // 二级:level=2 时作为末级可搜索,或允许选中父级且 level=3 时可搜索
if ((props.level === 2 || props.allowParentSelect) && l2.name.toLowerCase().includes(keyword)) { 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 } }) results.push({ path: `${l1.name}${l2.name}`, node: { id: l2.id, name: l2.name, level: l2.level } })
} }
if (props.level === 3) { if (props.level === 3) {
@@ -292,6 +297,14 @@ function selectLevel1(id: string) {
if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level }) if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level })
return 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) { if (props.allowParentSelect) {
const now = Date.now() const now = Date.now()
const lastTime = level1LastClickTime.value[id] || 0 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 }) if (l2) toggleItem({ id: l2.id, name: l2.name, level: l2.level })
return 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) { if (props.allowParentSelect) {
const now = Date.now() const now = Date.now()
const lastTime = level2LastClickTime.value[id] || 0 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) { function removeItem(item: SelectedNode) {
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id) selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
+2
View File
@@ -409,6 +409,7 @@
:level="2" :level="2"
:maxSelect="3" :maxSelect="3"
:allowParentSelect="false" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="filterTriggerStyle" :triggerStyle="filterTriggerStyle"
:displayStyle="filterDisplayStyle" :displayStyle="filterDisplayStyle"
@update:industryIds="onHomeIndustryChange" @update:industryIds="onHomeIndustryChange"
@@ -428,6 +429,7 @@
:level="3" :level="3"
:maxSelect="3" :maxSelect="3"
:allowParentSelect="false" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="filterTriggerStyle" :triggerStyle="filterTriggerStyle"
:displayStyle="filterDisplayStyle" :displayStyle="filterDisplayStyle"
@update:categoryIds="onHomeCategoryChange" @update:categoryIds="onHomeCategoryChange"
+6 -1
View File
@@ -3,7 +3,7 @@
<SideNav /> <SideNav />
<div class="job-detail__content"> <div class="job-detail__content">
<!-- 页面标题 + Tab 切换 --> <!-- 页面标题 + Tab 切换 -->
<JobPageHeader :activeTab="cachedTab" /> <JobPageHeader ref="jobPageHeaderRef" :activeTab="cachedTab" />
<!-- 主内容卡片 --> <!-- 主内容卡片 -->
<div class="job-detail__main-card"> <div class="job-detail__main-card">
<!-- 返回按钮 --> <!-- 返回按钮 -->
@@ -443,6 +443,9 @@ function goBack() {
/** AiChat 组件引用 */ /** AiChat 组件引用 */
const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null) const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null)
/** JobPageHeader 组件引用,用于刷新收藏/投递计数 */
const jobPageHeaderRef = ref<InstanceType<typeof JobPageHeader> | null>(null)
/** 问 Nova — 触发 AI 聊天,推送提示消息 */ /** 问 Nova — 触发 AI 聊天,推送提示消息 */
function handleAskNova() { function handleAskNova() {
aiChatRef.value?.triggerAsk() aiChatRef.value?.triggerAsk()
@@ -556,6 +559,8 @@ async function toggleFavorite() {
const wasFavorite = job.isFavorite const wasFavorite = job.isFavorite
job.isFavorite = !job.isFavorite job.isFavorite = !job.isFavorite
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功') ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
// 同步刷新 JobPageHeader 组件内的收藏计数
jobPageHeaderRef.value?.loadFavoriteCount()
// 同步收藏状态到 Jobs 页面缓存,返回时无需刷新列表即可看到最新状态 // 同步收藏状态到 Jobs 页面缓存,返回时无需刷新列表即可看到最新状态
const cache = store.state.jobListCache const cache = store.state.jobListCache
if (cache) { if (cache) {
+13 -4
View File
@@ -15,7 +15,7 @@
</div> </div>
<!-- 页面标题 + Tab 切换 --> <!-- 页面标题 + 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'"> <div v-if="isRecommendTab" class="jobs-page__filters-bar" :style="showAiRecommendTip?'':'margin-bottom:0.2rem'">
@@ -42,7 +42,8 @@
:categoryIds="selectedCategoryIds" :categoryIds="selectedCategoryIds"
:maxSelect="3" :maxSelect="3"
:level="3" :level="3"
:allowParentSelect="true" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="selectorTriggerStyle" :triggerStyle="selectorTriggerStyle"
:hoverStyle="selectorHoverStyle" :hoverStyle="selectorHoverStyle"
:displayStyle="selectorDisplayStyle" :displayStyle="selectorDisplayStyle"
@@ -55,7 +56,8 @@
:industryIds="selectedIndustryIds" :industryIds="selectedIndustryIds"
:maxSelect="3" :maxSelect="3"
:level="2" :level="2"
:allowParentSelect="true" :allowParentSelect="false"
:allowParentSelectClick="true"
:triggerStyle="selectorTriggerStyle" :triggerStyle="selectorTriggerStyle"
:hoverStyle="selectorHoverStyle" :hoverStyle="selectorHoverStyle"
:displayStyle="selectorDisplayStyle" :displayStyle="selectorDisplayStyle"
@@ -655,7 +657,7 @@ const aiCategoryNames = computed<string>(() => {
/** 是否显示 AI 推荐提示条 */ /** 是否显示 AI 推荐提示条 */
const showAiRecommendTip = computed(() => { 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 dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null) const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
/** JobPageHeader 组件引用,用于刷新收藏/投递计数 */
const jobPageHeaderRef = ref<InstanceType<typeof JobPageHeader> | null>(null)
/** 收藏/取消收藏岗位 */ /** 收藏/取消收藏岗位 */
async function toggleFavorite(job: JobItem) { async function toggleFavorite(job: JobItem) {
try { try {
@@ -1471,6 +1476,8 @@ async function toggleFavorite(job: JobItem) {
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功') ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
// 刷新收藏统计 // 刷新收藏统计
loadFavoriteCount() loadFavoriteCount()
// 同步刷新 JobPageHeader 组件内的收藏计数
jobPageHeaderRef.value?.loadFavoriteCount()
} }
} catch (e) { } catch (e) {
console.error('收藏操作失败', e) console.error('收藏操作失败', e)
@@ -1486,6 +1493,8 @@ async function removeFavoriteFromList(job: JobItem, index: number) {
total.value = Math.max(0, total.value - 1) total.value = Math.max(0, total.value - 1)
ElMessage.success('已取消收藏') ElMessage.success('已取消收藏')
loadFavoriteCount() loadFavoriteCount()
// 同步刷新 JobPageHeader 组件内的收藏计数
jobPageHeaderRef.value?.loadFavoriteCount()
} }
} catch (e) { } catch (e) {
console.error('取消收藏失败', e) console.error('取消收藏失败', e)
+4 -2
View File
@@ -126,7 +126,7 @@
<input ref="fileInputRef" type="file" accept=".pdf,.doc,.docx" class="login-view__file-input" @change="handleFileChange" /> <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> <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> </template>
<!-- 状态2解析中 parsing --> <!-- 状态2解析中 parsing -->
@@ -210,7 +210,9 @@
<template v-if="resumeState === 'failed'"> <template v-if="resumeState === 'failed'">
<div class="login-view__heading m0auto"> <div class="login-view__heading m0auto">
<h2 class="login-view__title">简历上传失败</h2> <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>
<!-- 失败面板 --> <!-- 失败面板 -->
<div class="login-view__failed-panel"> <div class="login-view__failed-panel">