feat(frontend): 添加前端基础框架与核心功能模块
- 初始化项目配置,新增npmrc和package.json文件 - 创建Vue 3应用入口,集成Element Plus UI库及图标插件 - 配置路由包括首页、招聘者管理、职位管理、候选人管理和定时任务五个模块 - 实现布局组件包含侧边栏菜单、顶部导航及主内容区 - 封装统一的API请求模块,包含招聘者、职位、候选人、定时任务和系统接口 - 开发Dashboard页面,实现系统概览及状态展示功能 - 开发候选人管理页面,包括搜索筛选、分页展示、详情查看及评分功能 - 开发职位管理页面,支持职位列表、搜索过滤、分页管理及关联评价方案 - 设计基础样式和交互效果,保证页面布局和风格一致性
This commit is contained in:
4
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/.npmrc
Normal file
4
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/.npmrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# pnpm 配置
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
auto-install-peers=true
|
||||||
13
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/index.html
Normal file
13
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>简历智能体 - HR管理系统</title>
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/package.json
Normal file
26
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "ylhp-hr-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@9.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.0",
|
||||||
|
"pinia": "^2.1.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"element-plus": "^2.5.0",
|
||||||
|
"@element-plus/icons-vue": "^2.3.0",
|
||||||
|
"dayjs": "^1.11.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"sass": "^1.70.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/App.vue
Normal file
18
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/api/api.js
Normal file
154
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/api/api.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: '',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截器 - 统一处理
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const data = response.data
|
||||||
|
if (data.code !== 200) {
|
||||||
|
return Promise.reject(new Error(data.msg || '请求失败'))
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 招聘者管理 API
|
||||||
|
export const recruiterApi = {
|
||||||
|
// 获取平台来源列表
|
||||||
|
getSources: () => api.get('/api/recruiters/sources'),
|
||||||
|
|
||||||
|
// 获取招聘者列表
|
||||||
|
getList: (params = {}) => api.get('/api/recruiters', { params }),
|
||||||
|
|
||||||
|
// 获取招聘者详情
|
||||||
|
getDetail: (id) => api.get(`/api/recruiters/${id}`),
|
||||||
|
|
||||||
|
// 创建招聘者
|
||||||
|
create: (data) => api.post('/api/recruiters', data),
|
||||||
|
|
||||||
|
// 自动注册
|
||||||
|
register: (data) => api.post('/api/recruiters/register', data),
|
||||||
|
|
||||||
|
// 更新招聘者
|
||||||
|
update: (id, data) => api.put(`/api/recruiters/${id}`, data),
|
||||||
|
|
||||||
|
// 删除招聘者
|
||||||
|
delete: (id) => api.delete(`/api/recruiters/${id}`),
|
||||||
|
|
||||||
|
// 启用招聘者
|
||||||
|
activate: (id) => api.post(`/api/recruiters/${id}/activate`),
|
||||||
|
|
||||||
|
// 停用招聘者
|
||||||
|
deactivate: (id) => api.post(`/api/recruiters/${id}/deactivate`),
|
||||||
|
|
||||||
|
// 同步招聘者
|
||||||
|
sync: (id) => api.post(`/api/recruiters/${id}/sync`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 职位管理 API
|
||||||
|
export const jobApi = {
|
||||||
|
// 获取职位列表
|
||||||
|
getList: (params = {}) => api.get('/api/jobs', { params }),
|
||||||
|
|
||||||
|
// 筛选职位
|
||||||
|
filter: (data) => api.post('/api/jobs/filter', data),
|
||||||
|
|
||||||
|
// 获取职位详情
|
||||||
|
getDetail: (id) => api.get(`/api/jobs/${id}`),
|
||||||
|
|
||||||
|
// 创建职位
|
||||||
|
create: (data) => api.post('/api/jobs', data),
|
||||||
|
|
||||||
|
// 更新职位
|
||||||
|
update: (id, data) => api.put(`/api/jobs/${id}`, data),
|
||||||
|
|
||||||
|
// 删除职位
|
||||||
|
delete: (id) => api.delete(`/api/jobs/${id}`),
|
||||||
|
|
||||||
|
// 关联评价方案
|
||||||
|
bindSchema: (id, schemaId) => api.post(`/api/jobs/${id}/bind-schema`, { evaluation_schema_id: schemaId }),
|
||||||
|
|
||||||
|
// 获取职位关联的评价方案
|
||||||
|
getSchema: (id) => api.get(`/api/jobs/${id}/schema`),
|
||||||
|
|
||||||
|
// 获取评价方案列表
|
||||||
|
getSchemaList: (params = {}) => api.get('/api/jobs/schemas/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 候选人管理 API
|
||||||
|
export const candidateApi = {
|
||||||
|
// 获取筛选通过的候选人
|
||||||
|
getFiltered: (params = {}) => api.get('/candidates/filtered', { params }),
|
||||||
|
|
||||||
|
// 筛选候选人
|
||||||
|
filter: (data) => api.post('/candidates/filter', data),
|
||||||
|
|
||||||
|
// 获取候选人详情
|
||||||
|
getDetail: (id) => api.get(`/candidates/${id}`),
|
||||||
|
|
||||||
|
// 标记候选人筛选状态
|
||||||
|
markFiltered: (data) => api.post('/candidates/mark-filtered', data),
|
||||||
|
|
||||||
|
// 更新候选人评分
|
||||||
|
updateScore: (data) => api.post('/candidates/update-score', data),
|
||||||
|
|
||||||
|
// 根据评分范围查询
|
||||||
|
getByScoreRange: (params) => api.get('/candidates/by-score-range', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时任务管理 API
|
||||||
|
export const schedulerApi = {
|
||||||
|
// 获取任务列表
|
||||||
|
getJobs: () => api.get('/api/scheduler/jobs'),
|
||||||
|
|
||||||
|
// 获取任务状态列表
|
||||||
|
getJobsStatus: () => api.get('/api/scheduler/jobs/status'),
|
||||||
|
|
||||||
|
// 获取单个任务状态
|
||||||
|
getJobStatus: (id) => api.get(`/api/scheduler/jobs/${id}/status`),
|
||||||
|
|
||||||
|
// 立即执行任务
|
||||||
|
runJob: (id) => api.post(`/api/scheduler/jobs/${id}/run`),
|
||||||
|
|
||||||
|
// 暂停任务
|
||||||
|
pauseJob: (id) => api.post(`/api/scheduler/jobs/${id}/pause`),
|
||||||
|
|
||||||
|
// 恢复任务
|
||||||
|
resumeJob: (id) => api.post(`/api/scheduler/jobs/${id}/resume`),
|
||||||
|
|
||||||
|
// 更新任务配置
|
||||||
|
updateConfig: (id, data) => api.put(`/api/scheduler/jobs/${id}/config`, data),
|
||||||
|
|
||||||
|
// 获取调度器状态
|
||||||
|
getStatus: () => api.get('/api/scheduler/status'),
|
||||||
|
|
||||||
|
// 启动调度器
|
||||||
|
start: () => api.post('/api/scheduler/start'),
|
||||||
|
|
||||||
|
// 停止调度器
|
||||||
|
stop: () => api.post('/api/scheduler/stop')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统 API
|
||||||
|
export const systemApi = {
|
||||||
|
// 获取首页信息
|
||||||
|
getHome: () => api.get('/'),
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
health: () => api.get('/health'),
|
||||||
|
|
||||||
|
// 获取API状态
|
||||||
|
getStatus: () => api.get('/api/status')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<el-aside width="220px" class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<el-icon size="28" color="#409EFF"><Briefcase /></el-icon>
|
||||||
|
<span class="logo-text">简历智能体</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-menu
|
||||||
|
:default-active="$route.path"
|
||||||
|
router
|
||||||
|
class="sidebar-menu"
|
||||||
|
background-color="#304156"
|
||||||
|
text-color="#bfcbd9"
|
||||||
|
active-text-color="#409EFF"
|
||||||
|
>
|
||||||
|
<el-menu-item v-for="route in menuRoutes" :key="route.path" :index="route.path">
|
||||||
|
<el-icon>
|
||||||
|
<component :is="route.meta.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ route.meta.title }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<el-container>
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<el-header class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<breadcrumb />
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-tooltip content="系统状态">
|
||||||
|
<el-icon size="20" :class="systemStatus"><CircleCheck /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<span class="version">v0.1.0</span>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<el-main class="main-content">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import router from '@/router'
|
||||||
|
import { systemApi } from '@/api/api'
|
||||||
|
|
||||||
|
const $route = useRoute()
|
||||||
|
const systemStatus = ref('status-healthy')
|
||||||
|
|
||||||
|
// 菜单路由
|
||||||
|
const menuRoutes = computed(() => {
|
||||||
|
return router.getRoutes()
|
||||||
|
.find(r => r.path === '/')
|
||||||
|
?.children.filter(r => r.meta) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查系统状态
|
||||||
|
const checkStatus = async () => {
|
||||||
|
try {
|
||||||
|
await systemApi.health()
|
||||||
|
systemStatus.value = 'status-healthy'
|
||||||
|
} catch {
|
||||||
|
systemStatus.value = 'status-error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkStatus()
|
||||||
|
setInterval(checkStatus, 30000)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-container {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: #304156;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
border-right: none;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-healthy {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/main.js
Normal file
22
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/main.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, { locale: zhCn })
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Layout from '@/components/Layout.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/dashboard',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: () => import('@/views/Dashboard.vue'),
|
||||||
|
meta: { title: '首页', icon: 'HomeFilled' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'recruiters',
|
||||||
|
name: 'Recruiters',
|
||||||
|
component: () => import('@/views/Recruiters.vue'),
|
||||||
|
meta: { title: '招聘者管理', icon: 'UserFilled' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'jobs',
|
||||||
|
name: 'Jobs',
|
||||||
|
component: () => import('@/views/Jobs.vue'),
|
||||||
|
meta: { title: '职位管理', icon: 'Briefcase' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'candidates',
|
||||||
|
name: 'Candidates',
|
||||||
|
component: () => import('@/views/Candidates.vue'),
|
||||||
|
meta: { title: '候选人管理', icon: 'Avatar' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'scheduler',
|
||||||
|
name: 'Scheduler',
|
||||||
|
component: () => import('@/views/Scheduler.vue'),
|
||||||
|
meta: { title: '定时任务', icon: 'Clock' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<div class="candidates-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">候选人管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :inline="true" :model="searchForm">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input v-model="searchForm.keyword" placeholder="姓名/公司/职位" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="LLM筛选">
|
||||||
|
<el-select v-model="searchForm.llm_filtered" placeholder="全部" clearable>
|
||||||
|
<el-option label="已通过" :value="true" />
|
||||||
|
<el-option label="未通过" :value="false" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="评分范围">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="11">
|
||||||
|
<el-input-number v-model="searchForm.min_score" :min="0" :max="100" placeholder="最低" style="width: 100%" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="2" style="text-align: center;">-</el-col>
|
||||||
|
<el-col :span="11">
|
||||||
|
<el-input-number v-model="searchForm.max_score" :min="0" :max="100" placeholder="最高" style="width: 100%" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-card>
|
||||||
|
<el-table :data="candidateList" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="name" label="姓名" width="100" fixed />
|
||||||
|
<el-table-column prop="gender" label="性别" width="70" />
|
||||||
|
<el-table-column prop="age" label="年龄" width="70" />
|
||||||
|
<el-table-column prop="current_company" label="当前公司" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="current_position" label="当前职位" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="education" label="学历" width="100" />
|
||||||
|
<el-table-column prop="location" label="地点" width="120" />
|
||||||
|
<el-table-column label="期望薪资" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.salary_min && row.salary_max">
|
||||||
|
{{ row.salary_min }}K-{{ row.salary_max }}K
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="LLM评分" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.llm_score" class="score-display">
|
||||||
|
<el-progress
|
||||||
|
:percentage="Math.round(row.llm_score)"
|
||||||
|
:color="getScoreColor(row.llm_score)"
|
||||||
|
:stroke-width="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray">未评分</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="筛选状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.llm_filtered ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.llm_filtered ? '已通过' : '未筛选' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" @click="handleView(row)">详情</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="handleScore(row)">评分</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 候选人详情对话框 -->
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="候选人详情" width="700px">
|
||||||
|
<el-descriptions :column="2" border v-if="currentCandidate">
|
||||||
|
<el-descriptions-item label="姓名">{{ currentCandidate.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">{{ currentCandidate.gender }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="年龄">{{ currentCandidate.age }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="学历">{{ currentCandidate.education }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="电话">{{ currentCandidate.phone || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ currentCandidate.email || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前公司" :span="2">{{ currentCandidate.current_company }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前职位" :span="2">{{ currentCandidate.current_position }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="地点">{{ currentCandidate.location }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="来源">{{ currentCandidate.source }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="LLM评分" :span="2">
|
||||||
|
<div v-if="currentCandidate.llm_score" class="detail-score">
|
||||||
|
<span class="score-value">{{ currentCandidate.llm_score }}</span>
|
||||||
|
<el-tag :type="currentCandidate.llm_filtered ? 'success' : 'info'" size="small">
|
||||||
|
{{ currentCandidate.llm_filtered ? '已通过筛选' : '未通过筛选' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else>未评分</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="评分详情" :span="2" v-if="currentCandidate.llm_score_details">
|
||||||
|
<pre class="score-details">{{ JSON.stringify(currentCandidate.llm_score_details, null, 2) }}</pre>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 评分对话框 -->
|
||||||
|
<el-dialog v-model="scoreDialogVisible" title="更新LLM评分" width="500px">
|
||||||
|
<el-form :model="scoreForm" :rules="scoreRules" ref="scoreFormRef" label-width="100px">
|
||||||
|
<el-form-item label="综合评分" prop="llm_score">
|
||||||
|
<el-slider v-model="scoreForm.llm_score" :max="100" show-stops :step="1" />
|
||||||
|
<div class="score-value">{{ scoreForm.llm_score }} 分</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="评分详情">
|
||||||
|
<el-input
|
||||||
|
v-model="scoreForm.llm_score_details"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder='{"专业能力": 85, "经验匹配": 90}'
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="scoreDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmitScore" :loading="submitting">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { candidateApi } from '@/api/api'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const candidateList = ref([])
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
llm_filtered: undefined,
|
||||||
|
min_score: undefined,
|
||||||
|
max_score: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 详情
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentCandidate = ref(null)
|
||||||
|
|
||||||
|
// 评分
|
||||||
|
const scoreDialogVisible = ref(false)
|
||||||
|
const scoreFormRef = ref()
|
||||||
|
const scoreForm = reactive({
|
||||||
|
candidate_id: '',
|
||||||
|
llm_score: 70,
|
||||||
|
llm_score_details: ''
|
||||||
|
})
|
||||||
|
const scoreRules = {
|
||||||
|
llm_score: [{ required: true, message: '请输入评分', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
let res
|
||||||
|
if (searchForm.min_score !== undefined && searchForm.max_score !== undefined) {
|
||||||
|
// 使用评分范围查询
|
||||||
|
res = await candidateApi.getByScoreRange({
|
||||||
|
min_score: searchForm.min_score,
|
||||||
|
max_score: searchForm.max_score,
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 使用筛选查询
|
||||||
|
res = await candidateApi.filter({
|
||||||
|
keyword: searchForm.keyword,
|
||||||
|
llm_filtered: searchForm.llm_filtered,
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
candidateList.value = res.data?.items || []
|
||||||
|
pagination.total = res.data?.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载数据失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.keyword = ''
|
||||||
|
searchForm.llm_filtered = undefined
|
||||||
|
searchForm.min_score = undefined
|
||||||
|
searchForm.max_score = undefined
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.pageSize = size
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = async (row) => {
|
||||||
|
try {
|
||||||
|
const res = await candidateApi.getDetail(row.id)
|
||||||
|
currentCandidate.value = res.data
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取详情失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评分
|
||||||
|
const handleScore = (row) => {
|
||||||
|
scoreForm.candidate_id = row.id
|
||||||
|
scoreForm.llm_score = row.llm_score || 70
|
||||||
|
scoreForm.llm_score_details = row.llm_score_details
|
||||||
|
? JSON.stringify(row.llm_score_details, null, 2)
|
||||||
|
: ''
|
||||||
|
scoreDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交评分
|
||||||
|
const handleSubmitScore = async () => {
|
||||||
|
const valid = await scoreFormRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
candidate_id: scoreForm.candidate_id,
|
||||||
|
llm_score: scoreForm.llm_score,
|
||||||
|
llm_score_details: scoreForm.llm_score_details
|
||||||
|
? JSON.parse(scoreForm.llm_score_details)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
await candidateApi.updateScore(data)
|
||||||
|
ElMessage.success('评分更新成功')
|
||||||
|
scoreDialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('提交失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getScoreColor = (score) => {
|
||||||
|
if (score >= 80) return '#67C23A'
|
||||||
|
if (score >= 60) return '#E6A23C'
|
||||||
|
return '#F56C6C'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.candidates-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-display {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-score {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-details {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<h2 class="page-title">系统概览</h2>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<el-row :gutter="20" class="stat-row">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #409EFF;">
|
||||||
|
<el-icon size="32" color="#fff"><UserFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ stats.recruiters }}</div>
|
||||||
|
<div class="stat-label">招聘者账号</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #67C23A;">
|
||||||
|
<el-icon size="32" color="#fff"><Briefcase /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ stats.jobs }}</div>
|
||||||
|
<div class="stat-label">职位数量</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #E6A23C;">
|
||||||
|
<el-icon size="32" color="#fff"><Avatar /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ stats.candidates }}</div>
|
||||||
|
<div class="stat-label">候选人</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-icon" style="background: #F56C6C;">
|
||||||
|
<el-icon size="32" color="#fff"><Star /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ stats.evaluations }}</div>
|
||||||
|
<div class="stat-label">评价记录</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 快捷操作 -->
|
||||||
|
<el-row :gutter="20" class="action-row">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>快捷操作</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<el-button type="primary" @click="$router.push('/recruiters')">
|
||||||
|
<el-icon><Plus /></el-icon>添加招聘者
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" @click="$router.push('/jobs')">
|
||||||
|
<el-icon><Plus /></el-icon>创建职位
|
||||||
|
</el-button>
|
||||||
|
<el-button type="warning" @click="$router.push('/scheduler')">
|
||||||
|
<el-icon><VideoPlay /></el-icon>启动任务
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>系统状态</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="system-status">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">API服务</span>
|
||||||
|
<el-tag :type="apiStatus === 'running' ? 'success' : 'danger'">
|
||||||
|
{{ apiStatus === 'running' ? '运行中' : '异常' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">调度器</span>
|
||||||
|
<el-tag :type="schedulerStatus.running ? 'success' : 'info'">
|
||||||
|
{{ schedulerStatus.running ? '运行中' : '已停止' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">任务数量</span>
|
||||||
|
<span class="status-value">{{ schedulerStatus.total_jobs || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { recruiterApi, schedulerApi, systemApi } from '@/api/api'
|
||||||
|
|
||||||
|
const stats = ref({
|
||||||
|
recruiters: 0,
|
||||||
|
jobs: 0,
|
||||||
|
candidates: 0,
|
||||||
|
evaluations: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiStatus = ref('running')
|
||||||
|
const schedulerStatus = ref({})
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
// 获取招聘者数量
|
||||||
|
const recruiterRes = await recruiterApi.getList()
|
||||||
|
stats.value.recruiters = recruiterRes.data?.total || 0
|
||||||
|
|
||||||
|
// 获取调度器状态
|
||||||
|
const schedulerRes = await schedulerApi.getStatus()
|
||||||
|
schedulerStatus.value = schedulerRes.data || {}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载统计数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkApiStatus = async () => {
|
||||||
|
try {
|
||||||
|
await systemApi.health()
|
||||||
|
apiStatus.value = 'running'
|
||||||
|
} catch {
|
||||||
|
apiStatus.value = 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStats()
|
||||||
|
checkApiStatus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #EBEEF5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
478
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/Jobs.vue
Normal file
478
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/Jobs.vue
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
<template>
|
||||||
|
<div class="jobs-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">职位管理</h2>
|
||||||
|
<el-button type="primary" @click="showAddDialog">
|
||||||
|
<el-icon><Plus /></el-icon>创建职位
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :inline="true" :model="searchForm">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input v-model="searchForm.keyword" placeholder="标题/部门" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||||
|
<el-option label="进行中" value="active" />
|
||||||
|
<el-option label="已暂停" value="paused" />
|
||||||
|
<el-option label="已关闭" value="closed" />
|
||||||
|
<el-option label="已归档" value="archived" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="评价方案">
|
||||||
|
<el-select v-model="searchForm.evaluation_schema_id" placeholder="全部" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="schema in schemaList"
|
||||||
|
:key="schema.id"
|
||||||
|
:label="schema.name"
|
||||||
|
:value="schema.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-card>
|
||||||
|
<el-table :data="jobList" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="title" label="职位标题" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="department" label="部门" width="120" />
|
||||||
|
<el-table-column prop="location" label="地点" width="120" />
|
||||||
|
<el-table-column label="薪资范围" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.salary_min && row.salary_max">
|
||||||
|
{{ row.salary_min }}K - {{ row.salary_max }}K
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray">面议</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="候选人" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-badge :value="row.new_candidate_count" class="item" v-if="row.new_candidate_count > 0">
|
||||||
|
<span>{{ row.candidate_count || 0 }}人</span>
|
||||||
|
</el-badge>
|
||||||
|
<span v-else>{{ row.candidate_count || 0 }}人</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="评价方案" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.evaluation_schema_id" class="schema-tag">
|
||||||
|
<el-tag size="small" type="success">
|
||||||
|
{{ getSchemaName(row.evaluation_schema_id) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="handleUnbindSchema(row)"
|
||||||
|
>
|
||||||
|
解除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button v-else link size="small" type="primary" @click="handleBindSchema(row)">
|
||||||
|
关联方案
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="last_sync_at" label="最后同步" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row.last_sync_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑职位' : '创建职位'"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="职位标题" prop="title">
|
||||||
|
<el-input v-model="form.title" placeholder="请输入职位标题" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="部门" prop="department">
|
||||||
|
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="工作地点" prop="location">
|
||||||
|
<el-input v-model="form.location" placeholder="请输入工作地点" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="薪资范围">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="11">
|
||||||
|
<el-input-number v-model="form.salary_min" :min="0" placeholder="最低" style="width: 100%" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="2" style="text-align: center;">-</el-col>
|
||||||
|
<el-col :span="11">
|
||||||
|
<el-input-number v-model="form.salary_max" :min="0" placeholder="最高" style="width: 100%" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="评价方案">
|
||||||
|
<el-select v-model="form.evaluation_schema_id" placeholder="请选择评价方案" clearable style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="schema in schemaList"
|
||||||
|
:key="schema.id"
|
||||||
|
:label="schema.name"
|
||||||
|
:value="schema.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="职位描述">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入职位描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 关联评价方案对话框 -->
|
||||||
|
<el-dialog v-model="bindDialogVisible" title="关联评价方案" width="500px">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="选择方案">
|
||||||
|
<el-select v-model="bindSchemaId" placeholder="请选择评价方案" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="schema in schemaList"
|
||||||
|
:key="schema.id"
|
||||||
|
:label="schema.name"
|
||||||
|
:value="schema.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="bindDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmBindSchema" :loading="binding">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { jobApi } from '@/api/api'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const jobList = ref([])
|
||||||
|
const schemaList = ref([])
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
evaluation_schema_id: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
department: '',
|
||||||
|
location: '',
|
||||||
|
salary_min: undefined,
|
||||||
|
salary_max: undefined,
|
||||||
|
evaluation_schema_id: '',
|
||||||
|
description: '',
|
||||||
|
source: 'boss',
|
||||||
|
source_id: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
title: [{ required: true, message: '请输入职位标题', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联方案
|
||||||
|
const bindDialogVisible = ref(false)
|
||||||
|
const bindSchemaId = ref('')
|
||||||
|
const currentJob = ref(null)
|
||||||
|
const binding = ref(false)
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 加载职位数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await jobApi.filter({
|
||||||
|
keyword: searchForm.keyword,
|
||||||
|
status: searchForm.status,
|
||||||
|
evaluation_schema_id: searchForm.evaluation_schema_id,
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize
|
||||||
|
})
|
||||||
|
jobList.value = res.data?.items || []
|
||||||
|
pagination.total = res.data?.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载数据失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载评价方案
|
||||||
|
const loadSchemas = async () => {
|
||||||
|
try {
|
||||||
|
const res = await jobApi.getSchemaList({ page_size: 100 })
|
||||||
|
schemaList.value = res.data?.items || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载评价方案失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.keyword = ''
|
||||||
|
searchForm.status = ''
|
||||||
|
searchForm.evaluation_schema_id = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.pageSize = size
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加
|
||||||
|
const showAddDialog = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
form.id = ''
|
||||||
|
form.title = ''
|
||||||
|
form.department = ''
|
||||||
|
form.location = ''
|
||||||
|
form.salary_min = undefined
|
||||||
|
form.salary_max = undefined
|
||||||
|
form.evaluation_schema_id = ''
|
||||||
|
form.description = ''
|
||||||
|
form.source_id = `manual_${Date.now()}`
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
form.id = row.id
|
||||||
|
form.title = row.title
|
||||||
|
form.department = row.department
|
||||||
|
form.location = row.location
|
||||||
|
form.salary_min = row.salary_min
|
||||||
|
form.salary_max = row.salary_max
|
||||||
|
form.evaluation_schema_id = row.evaluation_schema_id
|
||||||
|
form.description = row.description
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await jobApi.update(form.id, form)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await jobApi.create(form)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('提交失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该职位吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await jobApi.delete(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联评价方案
|
||||||
|
const handleBindSchema = (row) => {
|
||||||
|
currentJob.value = row
|
||||||
|
bindSchemaId.value = row.evaluation_schema_id || ''
|
||||||
|
bindDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmBindSchema = async () => {
|
||||||
|
if (!bindSchemaId.value) {
|
||||||
|
ElMessage.warning('请选择评价方案')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.value = true
|
||||||
|
try {
|
||||||
|
await jobApi.bindSchema(currentJob.value.id, bindSchemaId.value)
|
||||||
|
ElMessage.success('关联成功')
|
||||||
|
bindDialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('关联失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
binding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解除关联
|
||||||
|
const handleUnbindSchema = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要解除该职位的评价方案关联吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await jobApi.update(row.id, { evaluation_schema_id: null })
|
||||||
|
ElMessage.success('已解除关联')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('操作失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const map = { active: 'success', paused: 'warning', closed: 'info', archived: 'danger' }
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const map = { active: '进行中', paused: '已暂停', closed: '已关闭', archived: '已归档' }
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSchemaName = (id) => {
|
||||||
|
const schema = schemaList.value.find(s => s.id === id)
|
||||||
|
return schema?.name || id
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
loadSchemas()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jobs-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
<template>
|
||||||
|
<div class="recruiters-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">招聘者管理</h2>
|
||||||
|
<el-button type="primary" @click="showAddDialog">
|
||||||
|
<el-icon><Plus /></el-icon>添加招聘者
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :inline="true" :model="searchForm">
|
||||||
|
<el-form-item label="平台来源">
|
||||||
|
<el-select v-model="searchForm.source" placeholder="全部" clearable>
|
||||||
|
<el-option label="Boss直聘" value="boss" />
|
||||||
|
<el-option label="猎聘" value="liepin" />
|
||||||
|
<el-option label="智联招聘" value="zhilian" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-card>
|
||||||
|
<el-table :data="recruiterList" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="name" label="账号名称" min-width="150" />
|
||||||
|
<el-table-column prop="source" label="平台" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getSourceType(row.source)">
|
||||||
|
{{ getSourceLabel(row.source) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
|
||||||
|
{{ row.status === 'active' ? '启用' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="权益信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.privilege" class="privilege-info">
|
||||||
|
<div>VIP: {{ row.privilege.vip_level || '无' }}</div>
|
||||||
|
<div>剩余简历: {{ row.privilege.resume_view_count || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray">暂无权益信息</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="last_sync_at" label="最后同步" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row.last_sync_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="handleSync(row)">同步</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="row.status === 'active' ? 'warning' : 'success'"
|
||||||
|
@click="toggleStatus(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 'active' ? '停用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑招聘者' : '添加招聘者'"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="账号名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入账号名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="平台来源" prop="source">
|
||||||
|
<el-select v-model="form.source" placeholder="请选择平台" style="width: 100%">
|
||||||
|
<el-option label="Boss直聘" value="boss" />
|
||||||
|
<el-option label="猎聘" value="liepin" />
|
||||||
|
<el-option label="智联招聘" value="zhilian" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="WT Token" prop="wt_token">
|
||||||
|
<el-input
|
||||||
|
v-model="form.wt_token"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入WT Token"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 自动注册对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="registerDialogVisible"
|
||||||
|
title="自动注册招聘者"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form :model="registerForm" :rules="registerRules" ref="registerFormRef" label-width="100px">
|
||||||
|
<el-form-item label="平台来源" prop="source">
|
||||||
|
<el-select v-model="registerForm.source" placeholder="请选择平台" style="width: 100%">
|
||||||
|
<el-option label="Boss直聘" value="boss" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="WT Token" prop="wt_token">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.wt_token"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入WT Token,系统将自动获取账号信息"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="registerDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleRegister" :loading="registering">
|
||||||
|
自动注册
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { recruiterApi } from '@/api/api'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const recruiterList = ref([])
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
source: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
source: 'boss',
|
||||||
|
wt_token: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入账号名称', trigger: 'blur' }],
|
||||||
|
source: [{ required: true, message: '请选择平台来源', trigger: 'change' }],
|
||||||
|
wt_token: [{ required: true, message: '请输入WT Token', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动注册
|
||||||
|
const registerDialogVisible = ref(false)
|
||||||
|
const registerFormRef = ref()
|
||||||
|
const registerForm = reactive({
|
||||||
|
source: 'boss',
|
||||||
|
wt_token: ''
|
||||||
|
})
|
||||||
|
const registerRules = {
|
||||||
|
source: [{ required: true, message: '请选择平台来源', trigger: 'change' }],
|
||||||
|
wt_token: [{ required: true, message: '请输入WT Token', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const registering = ref(false)
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await recruiterApi.getList({
|
||||||
|
source: searchForm.source,
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize
|
||||||
|
})
|
||||||
|
recruiterList.value = res.data?.items || []
|
||||||
|
pagination.total = res.data?.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载数据失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.source = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.pageSize = size
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加
|
||||||
|
const showAddDialog = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
form.id = ''
|
||||||
|
form.name = ''
|
||||||
|
form.source = 'boss'
|
||||||
|
form.wt_token = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
form.id = row.id
|
||||||
|
form.name = row.name
|
||||||
|
form.source = row.source
|
||||||
|
form.wt_token = row.wt_token
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await recruiterApi.update(form.id, form)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await recruiterApi.create(form)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('提交失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该招聘者吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await recruiterApi.delete(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步
|
||||||
|
const handleSync = async (row) => {
|
||||||
|
try {
|
||||||
|
await recruiterApi.sync(row.id)
|
||||||
|
ElMessage.success('同步任务已触发')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('同步失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换状态
|
||||||
|
const toggleStatus = async (row) => {
|
||||||
|
try {
|
||||||
|
if (row.status === 'active') {
|
||||||
|
await recruiterApi.deactivate(row.id)
|
||||||
|
ElMessage.success('已停用')
|
||||||
|
} else {
|
||||||
|
await recruiterApi.activate(row.id)
|
||||||
|
ElMessage.success('已启用')
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动注册
|
||||||
|
const handleRegister = async () => {
|
||||||
|
const valid = await registerFormRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
registering.value = true
|
||||||
|
try {
|
||||||
|
const res = await recruiterApi.register(registerForm)
|
||||||
|
if (res.data?.success) {
|
||||||
|
ElMessage.success('注册成功: ' + res.data?.message)
|
||||||
|
registerDialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(res.data?.message || '注册失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('注册失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
registering.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getSourceType = (source) => {
|
||||||
|
const map = { boss: 'danger', liepin: 'primary', zhilian: 'success' }
|
||||||
|
return map[source] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSourceLabel = (source) => {
|
||||||
|
const map = { boss: 'Boss直聘', liepin: '猎聘', zhilian: '智联招聘' }
|
||||||
|
return map[source] || source
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.recruiters-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privilege-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scheduler-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">定时任务管理</h2>
|
||||||
|
<el-button-group>
|
||||||
|
<el-button type="success" @click="handleStart" :disabled="schedulerStatus.running">
|
||||||
|
<el-icon><VideoPlay /></el-icon>启动调度器
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" @click="handleStop" :disabled="!schedulerStatus.running">
|
||||||
|
<el-icon><VideoPause /></el-icon>停止调度器
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 调度器状态 -->
|
||||||
|
<el-row :gutter="20" class="status-row">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="status-card">
|
||||||
|
<div class="status-icon" :class="schedulerStatus.running ? 'running' : 'stopped'">
|
||||||
|
<el-icon size="32" color="#fff"><Timer /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-value">{{ schedulerStatus.running ? '运行中' : '已停止' }}</div>
|
||||||
|
<div class="status-label">调度器状态</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="status-card">
|
||||||
|
<div class="status-icon" style="background: #409EFF;">
|
||||||
|
<el-icon size="32" color="#fff"><List /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-value">{{ schedulerStatus.total_jobs || 0 }}</div>
|
||||||
|
<div class="status-label">任务总数</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="status-card">
|
||||||
|
<div class="status-icon" style="background: #67C23A;">
|
||||||
|
<el-icon size="32" color="#fff"><CircleCheck /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-value">{{ schedulerStatus.job_status_summary?.enabled || 0 }}</div>
|
||||||
|
<div class="status-label">已启用任务</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="status-card">
|
||||||
|
<div class="status-icon" style="background: #E6A23C;">
|
||||||
|
<el-icon size="32" color="#fff"><Loading /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-value">{{ schedulerStatus.job_status_summary?.running || 0 }}</div>
|
||||||
|
<div class="status-label">正在运行</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>任务列表</span>
|
||||||
|
<el-button type="primary" size="small" @click="loadData" :loading="loading">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="jobList" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="job_id" label="任务ID" min-width="150" />
|
||||||
|
<el-table-column prop="name" label="任务名称" min-width="150" />
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.enabled ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="运行状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_running ? 'warning' : 'info'" size="small" effect="dark">
|
||||||
|
{{ row.is_running ? '运行中' : '空闲' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="执行统计" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="stats">
|
||||||
|
<el-tag size="small" type="success">成功: {{ row.success_count || 0 }}</el-tag>
|
||||||
|
<el-tag size="small" type="danger">失败: {{ row.fail_count || 0 }}</el-tag>
|
||||||
|
<el-tag size="small" type="info">总计: {{ row.run_count || 0 }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="last_run_time" label="最后执行" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row.last_run_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="next_run_time" label="下次执行" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatTime(row.next_run_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" type="primary" @click="handleRun(row)" :loading="row.is_running">
|
||||||
|
<el-icon><VideoPlay /></el-icon>执行
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="row.enabled ? 'warning' : 'success'"
|
||||||
|
@click="toggleJobStatus(row)"
|
||||||
|
:disabled="row.is_running"
|
||||||
|
>
|
||||||
|
{{ row.enabled ? '暂停' : '恢复' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="handleConfig(row)">配置</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 配置对话框 -->
|
||||||
|
<el-dialog v-model="configDialogVisible" title="任务配置" width="500px">
|
||||||
|
<el-form :model="configForm" label-width="120px">
|
||||||
|
<el-form-item label="任务ID">
|
||||||
|
<el-input v-model="configForm.job_id" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="configForm.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="执行间隔(分钟)">
|
||||||
|
<el-input-number v-model="configForm.interval_minutes" :min="1" :max="1440" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="configDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmitConfig" :loading="configuring">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { schedulerApi } from '@/api/api'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const jobList = ref([])
|
||||||
|
const schedulerStatus = ref({})
|
||||||
|
|
||||||
|
// 配置对话框
|
||||||
|
const configDialogVisible = ref(false)
|
||||||
|
const configuring = ref(false)
|
||||||
|
const configForm = reactive({
|
||||||
|
job_id: '',
|
||||||
|
enabled: true,
|
||||||
|
interval_minutes: 30
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [jobsRes, statusRes] = await Promise.all([
|
||||||
|
schedulerApi.getJobsStatus(),
|
||||||
|
schedulerApi.getStatus()
|
||||||
|
])
|
||||||
|
jobList.value = jobsRes.data || []
|
||||||
|
schedulerStatus.value = statusRes.data || {}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载数据失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动调度器
|
||||||
|
const handleStart = async () => {
|
||||||
|
try {
|
||||||
|
await schedulerApi.start()
|
||||||
|
ElMessage.success('调度器已启动')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('启动失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止调度器
|
||||||
|
const handleStop = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要停止调度器吗?正在运行的任务将被中断。', '警告', {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
await schedulerApi.stop()
|
||||||
|
ElMessage.success('调度器已停止')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('停止失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行任务
|
||||||
|
const handleRun = async (row) => {
|
||||||
|
try {
|
||||||
|
await schedulerApi.runJob(row.job_id)
|
||||||
|
ElMessage.success('任务已开始执行')
|
||||||
|
setTimeout(loadData, 1000)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('执行失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换任务状态
|
||||||
|
const toggleJobStatus = async (row) => {
|
||||||
|
try {
|
||||||
|
if (row.enabled) {
|
||||||
|
await schedulerApi.pauseJob(row.job_id)
|
||||||
|
ElMessage.success('任务已暂停')
|
||||||
|
} else {
|
||||||
|
await schedulerApi.resumeJob(row.job_id)
|
||||||
|
ElMessage.success('任务已恢复')
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开配置对话框
|
||||||
|
const handleConfig = (row) => {
|
||||||
|
configForm.job_id = row.job_id
|
||||||
|
configForm.enabled = row.enabled
|
||||||
|
configForm.interval_minutes = 30
|
||||||
|
configDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交配置
|
||||||
|
const handleSubmitConfig = async () => {
|
||||||
|
configuring.value = true
|
||||||
|
try {
|
||||||
|
await schedulerApi.updateConfig(configForm.job_id, {
|
||||||
|
enabled: configForm.enabled,
|
||||||
|
interval_minutes: configForm.interval_minutes
|
||||||
|
})
|
||||||
|
ElMessage.success('配置已更新')
|
||||||
|
configDialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('更新失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
configuring.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const formatTime = (time) => {
|
||||||
|
return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
// 自动刷新
|
||||||
|
setInterval(loadData, 10000)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scheduler-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.running {
|
||||||
|
background: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.stopped {
|
||||||
|
background: #F56C6C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/vite.config.js
Normal file
25
src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/vite.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/candidates': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user