feat(recruiter-ui): UX overhaul — nav grouping, workflow order, deduplication

Squashed from 3 commits on feature/recruiter-ui-ux-overhaul.

Changes based on 4-way parallel subagent design review + copilot review cycle:

**Nav**: 4 labeled groups replacing flat list — 账号配置 (login/LLM first) → 职位配置 → 自动化执行 → 集成与工具. Rename debug tool to 调试与测试工具.

**BossAutoBrowseAndChat**: Remove broken cross-ref text claiming per-job greeting overrides exist in 职位配置 (they don't).

**BossChatPage + BossAutoSequence**: Expand job sequence table to 3 columns (enabled/runRecommend/runChat) and normalize missing sequence fields from old configs to default true — consistent with the automation runner's own `!== false` logic.

**BossDebugTool**: Move browser controls inside Tab A (Tab B LLM screening doesn't need the browser). Clarify Tab B label. Add ephemeral-rubric warning with link to 职位配置 for persistence.

**BossJobConfig**: Merge work-exp and salary min/max from 4 separate checkbox rows into 2 combined range rows. Change resume-filter section scope tag to warning-type.

**BossLlmConfig**: Move 各用途默认模型 from bottom to top with isLoading guard on empty-state alert.

**BossAutoSequence**: Add pre-flight navigation links to browse/chat config pages.
This commit is contained in:
rqi14
2026-05-10 22:03:06 +08:00
committed by GitHub
parent 2456c5e818
commit 67767484ca
9 changed files with 212 additions and 121 deletions

View File

@@ -3,22 +3,22 @@
<div class="main__wrap">
<el-form ref="formRef" :model="formContent" label-position="top">
<el-card class="config-section">
<el-form-item label="招呼语(全局默认)" prop="autoChat.greetingMessage">
<el-form-item label="招呼语" prop="autoChat.greetingMessage">
<el-input
v-model="formContent.autoChat.greetingMessage"
type="textarea"
:autosize="{ minRows: 1 }"
placeholder="向候选人发送的第一条消息(各职位可在「职位配置」页覆盖)"
placeholder="向候选人发送的第一条消息"
/>
</el-form-item>
<el-form-item label="每次最多开聊人数(全局默认)" prop="autoChat.maxChatPerRun">
<el-form-item label="每次最多开聊人数" prop="autoChat.maxChatPerRun">
<el-input-number
v-model="formContent.autoChat.maxChatPerRun"
:min="1"
:max="200"
controls-position="right"
/>
<div class="form-tip">单轮运行中最多向多少人发送招呼各职位可在职位配置页覆盖</div>
<div class="form-tip">单轮运行中最多向多少人发送招呼</div>
</el-form-item>
<el-form-item label="两次开聊间隔(毫秒)">
<div class="range-input-wrap">

View File

@@ -43,8 +43,12 @@
<span>自动顺序执行</span>
</template>
<p class="desc">
依次执行推荐牛人 - 自动开聊沟通页两个任务配置分别在对应页面中设
按队列逐职位自动执行推荐牛人沟通页两个任务各自的运行策略请分别在对应页面
</p>
<div class="preflight-links">
<RouterLink :to="{ name: 'BossAutoBrowseAndChat' }"> 推荐牛人 - 自动开聊运行策略配置</RouterLink>
<RouterLink :to="{ name: 'BossChatPage' }"> 沟通运行策略配置</RouterLink>
</div>
<div class="action-bar">
<el-button type="primary" :loading="isSaving" @click="handleSubmit">
开始自动顺序执行
@@ -96,6 +100,7 @@
<script lang="ts" setup>
import { ref, onMounted, onActivated } from 'vue'
import { RouterLink } from 'vue-router'
import { ElMessage } from 'element-plus'
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
import { RUNNING_STATUS_ENUM } from '../../../../../common/enums/auto-start-chat'
@@ -123,7 +128,14 @@ const isSavingQueue = ref(false)
const loadJobsList = async () => {
try {
const result = await ipcRenderer.invoke('fetch-boss-jobs-config')
jobsList.value = result?.jobs ?? []
jobsList.value = (result?.jobs ?? []).map((j: any) => ({
...j,
sequence: {
enabled: j.sequence?.enabled ?? true,
runRecommend: j.sequence?.runRecommend ?? true,
runChat: j.sequence?.runChat ?? true
}
}))
} catch (err) {
console.error(err)
}
@@ -187,12 +199,26 @@ const handleStopButtonClick = async () => {
}
.desc {
margin: 0 0 1em;
margin: 0 0 0.5em;
font-size: 14px;
color: #606266;
line-height: 1.6;
}
.preflight-links {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 1em;
a {
font-size: 13px;
color: #2faa9e;
text-decoration: none;
&:hover { text-decoration: underline; }
}
}
.queue-save-bar {
display: flex;
align-items: center;

View File

@@ -22,9 +22,19 @@
<el-checkbox v-model="row.sequence.enabled" />
</template>
</el-table-column>
<el-table-column label="执行推荐牛人" width="120" align="center">
<template #default="{ row }">
<el-checkbox v-model="row.sequence.runRecommend" :disabled="!row.sequence.enabled" />
</template>
</el-table-column>
<el-table-column label="执行沟通页" width="110" align="center">
<template #default="{ row }">
<el-checkbox v-model="row.sequence.runChat" :disabled="!row.sequence.enabled" />
</template>
</el-table-column>
</el-table>
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
勾选的职位将在处理沟通页时被依次扫描若全部不勾选则不处理任何职位
勾选的职位将在处理沟通页时被依次扫描若全部不勾选则不处理任何职位执行推荐牛人执行沟通页列在自动顺序执行模式下生效
</div>
</template>
</el-card>
@@ -164,7 +174,14 @@ const loadData = async () => {
formContent.chatPage.runOnceAfterComplete = chatPage.runOnceAfterComplete ?? false
formContent.chatPage.keepBrowserOpenAfterRun = chatPage.keepBrowserOpenAfterRun ?? false
formContent.chatPage.rerunIntervalMs = chatPage.rerunIntervalMs ?? 3000
jobsList.value = jobsResult?.jobs ?? []
jobsList.value = (jobsResult?.jobs ?? []).map((j: any) => ({
...j,
sequence: {
enabled: j.sequence?.enabled ?? true,
runRecommend: j.sequence?.runRecommend ?? true,
runChat: j.sequence?.runChat ?? true
}
}))
} catch (err) {
console.error(err)
}

View File

@@ -1,33 +1,32 @@
<template>
<div class="debug-tool__wrap">
<div class="main__wrap">
<!-- 顶部控制栏 -->
<el-card class="section">
<div class="section-title">招聘端调试工具</div>
<div class="section-desc">
启动浏览器并打开沟通页在右侧手动选中一条会话再用下方按钮测试各项功能<br />
<strong>Tab A</strong> 需要浏览器已就绪<strong>Tab BLLM 筛选</strong>运行评估生成 Rubric不需要浏览器
</div>
<div class="action-bar">
<el-button
type="primary"
:loading="isLaunching"
:disabled="isReady"
@click="handleLaunch"
>
{{ isReady ? '浏览器已启动' : '启动浏览器' }}
</el-button>
<el-button :disabled="!isReady" @click="handleClose">关闭浏览器</el-button>
<el-tag v-if="isReady" type="success">已就绪</el-tag>
<el-tag v-else-if="isLaunching" type="warning">启动中...</el-tag>
<el-tag v-else type="info">未启动</el-tag>
</div>
</el-card>
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" class="debug-tabs">
<!-- Tab A: 简历操作 -->
<el-tab-pane label="简历操作" name="resume">
<el-tab-pane label="简历操作(需要浏览器)" name="resume">
<!-- 浏览器控制栏仅在 Tab A 显示 -->
<el-card class="section">
<div class="section-title">浏览器控制</div>
<div class="section-desc">
启动浏览器并打开沟通页在右侧手动选中一条会话再用下方按钮测试各项功能
</div>
<div class="action-bar">
<el-button
type="primary"
:loading="isLaunching"
:disabled="isReady"
@click="handleLaunch"
>
{{ isReady ? '浏览器已启动' : '启动浏览器' }}
</el-button>
<el-button :disabled="!isReady" @click="handleClose">关闭浏览器</el-button>
<el-tag v-if="isReady" type="success">已就绪</el-tag>
<el-tag v-else-if="isLaunching" type="warning">启动中...</el-tag>
<el-tag v-else type="info">未启动</el-tag>
</div>
</el-card>
<el-card class="section" :class="{ disabled: !isReady }">
<div class="section-title">当前会话操作</div>
<div class="cmd-grid">
@@ -48,13 +47,16 @@
</el-tab-pane>
<!-- Tab B: LLM 筛选 -->
<el-tab-pane label="LLM 筛选" name="llm">
<el-tab-pane label="LLM 筛选测试区域1/3 无需浏览器)" name="llm">
<!-- 区域 1生成 Rubric工作流起点 -->
<el-card class="section">
<div class="section-title">区域 1生成 Rubric</div>
<div class="section-desc">
输入 JD 自动生成评分标准生成后可直接编辑 JSON再点用于评估传到区域 2
输入 JD 自动生成评分标准生成后可直接编辑 JSON再点用于评估传到区域 3<br />
<strong>注意</strong>此处生成的 Rubric 仅用于测试不会自动保存如需持久保存请在
<RouterLink :to="{ name: 'BossJobConfig' }" style="color: #2faa9e;">职位配置</RouterLink>
页面为具体职位生成并保存
</div>
<div class="llm-label">岗位描述JD</div>
@@ -277,6 +279,7 @@
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, computed } from 'vue'
import { RouterLink } from 'vue-router'
import { ElMessage } from 'element-plus'
const { ipcRenderer } = electron

View File

@@ -27,8 +27,8 @@
<el-form :model="job" label-position="top" class="job-form">
<!-- 两页通用筛选 -->
<div class="section-label">
<span>候选人筛选条件</span>
<el-tag size="small">推荐牛人页 + 沟通页</el-tag>
<span>候选人基础筛选</span>
<el-tag size="small">推荐牛人页 + 沟通页 均生效</el-tag>
</div>
<div class="filter-row">
@@ -70,58 +70,58 @@
</div>
<div class="filter-row">
<el-checkbox v-model="job.filter.expectWorkExpMinEnabled" />
<el-form-item label="工作经验下限(年)" class="flex-1">
<el-input-number
v-model="job.filter.expectWorkExpRange[0]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectWorkExpMinEnabled"
placeholder="最少"
/>
<el-checkbox
:model-value="job.filter.expectWorkExpMinEnabled || job.filter.expectWorkExpMaxEnabled"
@change="(v) => { job.filter.expectWorkExpMinEnabled = v; job.filter.expectWorkExpMaxEnabled = v }"
/>
<el-form-item label="工作经验范围(年)" class="flex-1">
<div class="range-input-wrap">
<el-input-number
v-model="job.filter.expectWorkExpRange[0]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectWorkExpMinEnabled && !job.filter.expectWorkExpMaxEnabled"
placeholder="最少"
/>
<span class="range-sep">~</span>
<el-input-number
v-model="job.filter.expectWorkExpRange[1]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectWorkExpMinEnabled && !job.filter.expectWorkExpMaxEnabled"
placeholder="最多99=不限)"
/>
</div>
</el-form-item>
</div>
<div class="filter-row">
<el-checkbox v-model="job.filter.expectWorkExpMaxEnabled" />
<el-form-item label="工作经验上限(年)" class="flex-1">
<el-input-number
v-model="job.filter.expectWorkExpRange[1]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectWorkExpMaxEnabled"
placeholder="最多99=不限)"
/>
</el-form-item>
</div>
<div class="filter-row">
<el-checkbox v-model="job.filter.expectSalaryMinEnabled" />
<el-form-item label="期望薪资下限K/月)" class="flex-1">
<el-input-number
v-model="job.filter.expectSalaryRange[0]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectSalaryMinEnabled"
placeholder="最低0=不限)"
/>
<el-checkbox
:model-value="job.filter.expectSalaryMinEnabled || job.filter.expectSalaryMaxEnabled"
@change="(v) => { job.filter.expectSalaryMinEnabled = v; job.filter.expectSalaryMaxEnabled = v }"
/>
<el-form-item label="期望薪资范围K/月)" class="flex-1">
<div class="range-input-wrap">
<el-input-number
v-model="job.filter.expectSalaryRange[0]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectSalaryMinEnabled && !job.filter.expectSalaryMaxEnabled"
placeholder="最低"
/>
<span class="range-sep">~</span>
<el-input-number
v-model="job.filter.expectSalaryRange[1]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectSalaryMinEnabled && !job.filter.expectSalaryMaxEnabled"
placeholder="最高"
/>
</div>
<div class="form-tip"> 8 表示 8K 8000 表示 8000 /会自动换算成 8K</div>
</el-form-item>
</div>
<div class="filter-row">
<el-checkbox v-model="job.filter.expectSalaryMaxEnabled" />
<el-form-item label="期望薪资上限K/月)" class="flex-1">
<el-input-number
v-model="job.filter.expectSalaryRange[1]"
:min="0"
controls-position="right"
:disabled="!job.filter.expectSalaryMaxEnabled"
placeholder="最高0=不限)"
/>
</el-form-item>
</div>
<div class="filter-row">
<el-checkbox style="visibility: hidden" />
<el-form-item label="薪资为「面议」时" class="flex-1">
@@ -133,16 +133,17 @@
<el-option value="exclude" label="不通过(排除)" />
<el-option value="include" label="通过(不因薪资排除)" />
</el-select>
<div class="form-tip">仅在启用薪资范围筛选时生效</div>
</el-form-item>
</div>
<!-- 沟通页专属 -->
<div class="section-label">
<span>简历全文筛选</span>
<el-tag size="small" type="success">仅沟通页</el-tag>
<el-tag size="small" type="warning">仅沟通页生效推荐牛人页不使用此项</el-tag>
</div>
<div class="form-tip resume-filter-tip">勾选一个或多个筛选模块全部不勾选则不筛选</div>
<div class="form-tip resume-filter-tip">勾选一个或多个筛选模块全部不勾选则不筛选简历全文</div>
<div class="filter-row resume-module-row">
<el-checkbox v-model="job.filter.resumeKeywordsEnabled" label="关键词匹配" />
@@ -906,6 +907,17 @@ function addDimension(job: JobItem) {
margin-left: 24px;
}
.range-input-wrap {
display: flex;
align-items: center;
gap: 8px;
.range-sep {
color: #999;
flex-shrink: 0;
}
}
.form-tip {
font-size: 12px;
color: #909399;

View File

@@ -8,6 +8,41 @@
</div>
</div>
<!-- 各用途默认模型前置显示方便快速查看当前生效模型 -->
<el-card class="section" style="margin-bottom: 0">
<div class="section-title">各用途默认模型</div>
<div class="section-desc">指定每种用途优先使用哪个模型未选则跟随第一个启用的模型</div>
<div class="form-row-2">
<el-form-item
v-for="purpose in purposes"
:key="purpose.key"
:label="purpose.label"
>
<el-select
v-model="purposeDefaultModelId[purpose.key]"
clearable
placeholder="(跟随第一个启用的模型)"
style="width: 100%"
>
<el-option
v-for="m in allEnabledModels"
:key="m.id"
:label="m.displayName"
:value="m.id"
/>
</el-select>
</el-form-item>
</div>
<el-alert
v-if="!isLoading && allEnabledModels.length === 0"
type="info"
show-icon
:closable="false"
title="尚未添加任何启用的模型,请在下方添加服务商和模型后再设置默认模型"
style="margin-top: 4px"
/>
</el-card>
<!-- Provider 列表 -->
<div v-if="providers.length" class="provider-list">
<el-card
@@ -149,33 +184,6 @@
</el-dropdown>
</div>
<!-- 各用途默认模型 -->
<el-card class="section" style="margin-top: 16px">
<div class="section-title">各用途默认模型</div>
<div class="section-desc">当同一用途有多个模型时指定优先使用哪一个</div>
<div class="form-row-2">
<el-form-item
v-for="purpose in purposes"
:key="purpose.key"
:label="purpose.label"
>
<el-select
v-model="purposeDefaultModelId[purpose.key]"
clearable
placeholder="(跟随第一个启用的模型)"
style="width: 100%"
>
<el-option
v-for="m in allEnabledModels"
:key="m.id"
:label="m.displayName"
:value="m.id"
/>
</el-select>
</el-form-item>
</div>
</el-card>
<!-- 操作栏 -->
<div class="action-bar">
<el-button :loading="isSaving" type="primary" @click="handleSave">保存配置</el-button>
@@ -218,6 +226,7 @@ interface ProviderEntry {
const providers = ref<ProviderEntry[]>([])
const purposeDefaultModelId = ref<Record<string, string>>({})
const isSaving = ref(false)
const isLoading = ref(true)
const purposes = [
{ key: 'resume_screening', label: '简历筛选' },
@@ -314,6 +323,8 @@ onMounted(async () => {
purposeDefaultModelId.value = config?.purposeDefaultModelId ?? {}
} catch (err) {
console.error('[BossLlmConfig] 加载配置失败', err)
} finally {
isLoading.value = false
}
})

View File

@@ -2,9 +2,24 @@
<div class="group-item">
<div class="group-title">招聘BOSS</div>
<div flex flex-col class="link-list">
<div class="nav-sub-label">账号配置</div>
<a href="javascript:void(0)" @click="handleClickRecruiterLogin">
编辑登录凭据<TopRight w-1em h-1em mr10px />
</a>
<RouterLink :to="{ name: 'BossLlmConfig' }">
配置大语言模型
</RouterLink>
<a href="javascript:void(0)" @click="handleLaunchRecruiterBossSite">
手动逛逛<TopRight w-1em h-1em mr10px />
</a>
<div class="nav-sub-label">职位配置</div>
<RouterLink :to="{ name: 'BossJobConfig' }">
职位配置
</RouterLink>
<div class="nav-sub-label">自动化执行</div>
<RouterLink :to="{ name: 'BossAutoBrowseAndChat' }">
推荐牛人 - 自动开聊
</RouterLink>
@@ -14,21 +29,15 @@
<RouterLink :to="{ name: 'BossAutoSequence' }">
自动顺序执行
</RouterLink>
<div class="nav-sub-label">集成与工具</div>
<RouterLink :to="{ name: 'WebhookIntegration' }">
Webhook / 外部集成
</RouterLink>
<a href="javascript:void(0)" @click="handleClickRecruiterLogin">
编辑登录凭据<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleLaunchRecruiterBossSite">
手动逛逛<TopRight w-1em h-1em mr10px />
</a>
<RouterLink :to="{ name: 'BossDebugTool' }">
招聘端调试工具
</RouterLink>
<RouterLink :to="{ name: 'BossLlmConfig' }">
配置大语言模型
试与测试工具
</RouterLink>
</div>
</div>
</template>

View File

@@ -5,6 +5,19 @@
padding: 0.25em 0;
}
.link-list {
.nav-sub-label {
color: #b0bcba;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 0.6em 0 0.15em 1em;
margin-top: 0.2em;
&:first-of-type {
margin-top: 0;
padding-top: 0.15em;
}
}
a {
display: flex;
align-items: center;

View File

@@ -175,7 +175,7 @@ const routes: Array<RouteRecordRaw> = [
path: 'BossDebugTool',
component: () => import('@renderer/page/MainLayout/BossDebugTool/index.vue'),
meta: {
title: '招聘端调试工具'
title: '调试与测试工具'
}
},
{