feat(notification-channels): 新增通知渠道管理功能
- 后端API新增评价方案列表接口及通知渠道相关接口 - 所有候选人相关API路径添加/api前缀 - 系统首页接口更新候选人路径为/api/candidates - CandidateMapper和JobMapper排序逻辑调整以兼容MySQL null值排序 - 前端candidateApi接口路径添加/api前缀 - 新增notificationChannelApi管理通知渠道,包括增删改查、启用停用及招聘者绑定管理 - 路由新增通知渠道管理页面入口 - 实现NotificationChannels.vue通知渠道的增删改查、搜索筛选、分页、启用停用及招聘者绑定管理功能 - Recruiters.vue中新增通知渠道绑定对话框及绑定相关逻辑,支持招聘者绑定通知渠道管理 - controller/schemas.py新增分页参数PaginationParams及重建模型修正前向引用 - UI组件调整及新增对应表格列、表单校验规则和界面交互逻辑
This commit is contained in:
@@ -15,7 +15,7 @@ from ..schemas import (
|
|||||||
from ...mapper.candidate_mapper import CandidateMapper
|
from ...mapper.candidate_mapper import CandidateMapper
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/candidates", tags=["候选人管理"])
|
router = APIRouter(prefix="/api/candidates", tags=["候选人管理"])
|
||||||
|
|
||||||
|
|
||||||
def _candidate_to_response(candidate) -> CandidateResponse:
|
def _candidate_to_response(candidate) -> CandidateResponse:
|
||||||
|
|||||||
@@ -45,6 +45,45 @@ def _job_to_response(job: Job) -> JobPositionResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schemas/list", response_model=BaseResponse[EvaluationSchemaListResponse])
|
||||||
|
async def list_evaluation_schemas(
|
||||||
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="每页数量")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取评价方案列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[EvaluationSchemaListResponse]: 统一响应格式的评价方案列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
schema_mapper = EvaluationMapper()
|
||||||
|
schemas, total = schema_mapper.find_all_schemas(page=page, page_size=page_size)
|
||||||
|
|
||||||
|
items = [
|
||||||
|
EvaluationSchemaResponse(
|
||||||
|
id=schema.id,
|
||||||
|
name=schema.name,
|
||||||
|
description=schema.description,
|
||||||
|
dimensions=schema.dimensions,
|
||||||
|
weights=schema.weights,
|
||||||
|
is_default=schema.is_default,
|
||||||
|
created_at=schema.created_at,
|
||||||
|
updated_at=schema.updated_at
|
||||||
|
)
|
||||||
|
for schema in schemas
|
||||||
|
]
|
||||||
|
|
||||||
|
response = EvaluationSchemaListResponse(total=total, items=items)
|
||||||
|
return BaseResponse.success(data=response, msg="获取评价方案列表成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取评价方案列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=BaseResponse[JobPositionListResponse])
|
@router.get("", response_model=BaseResponse[JobPositionListResponse])
|
||||||
async def list_jobs(
|
async def list_jobs(
|
||||||
source: Optional[str] = Query(None, description="平台来源"),
|
source: Optional[str] = Query(None, description="平台来源"),
|
||||||
@@ -368,42 +407,3 @@ async def get_job_evaluation_schema(job_id: str):
|
|||||||
return BaseResponse.success(data=response, msg="获取评价方案成功")
|
return BaseResponse.success(data=response, msg="获取评价方案成功")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return BaseResponse.error(msg=f"获取评价方案失败: {str(e)}")
|
return BaseResponse.error(msg=f"获取评价方案失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schemas/list", response_model=BaseResponse[EvaluationSchemaListResponse])
|
|
||||||
async def list_evaluation_schemas(
|
|
||||||
page: int = Query(1, ge=1, description="页码"),
|
|
||||||
page_size: int = Query(20, ge=1, le=100, description="每页数量")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取评价方案列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
page: 页码
|
|
||||||
page_size: 每页数量
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BaseResponse[EvaluationSchemaListResponse]: 统一响应格式的评价方案列表
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
schema_mapper = EvaluationMapper()
|
|
||||||
schemas, total = schema_mapper.find_all_schemas(page=page, page_size=page_size)
|
|
||||||
|
|
||||||
items = [
|
|
||||||
EvaluationSchemaResponse(
|
|
||||||
id=schema.id,
|
|
||||||
name=schema.name,
|
|
||||||
description=schema.description,
|
|
||||||
dimensions=schema.dimensions,
|
|
||||||
weights=schema.weights,
|
|
||||||
is_default=schema.is_default,
|
|
||||||
created_at=schema.created_at,
|
|
||||||
updated_at=schema.updated_at
|
|
||||||
)
|
|
||||||
for schema in schemas
|
|
||||||
]
|
|
||||||
|
|
||||||
response = EvaluationSchemaListResponse(total=total, items=items)
|
|
||||||
return BaseResponse.success(data=response, msg="获取评价方案列表成功")
|
|
||||||
except Exception as e:
|
|
||||||
return BaseResponse.error(msg=f"获取评价方案列表失败: {str(e)}")
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async def root():
|
|||||||
"endpoints": {
|
"endpoints": {
|
||||||
"recruiters": "/api/recruiters",
|
"recruiters": "/api/recruiters",
|
||||||
"jobs": "/api/jobs",
|
"jobs": "/api/jobs",
|
||||||
"candidates": "/candidates",
|
"candidates": "/api/candidates",
|
||||||
"scheduler": "/api/scheduler",
|
"scheduler": "/api/scheduler",
|
||||||
"health": "/health"
|
"health": "/health"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ API共享Schema定义
|
|||||||
|
|
||||||
集中定义所有API请求和响应的数据模型
|
集中定义所有API请求和响应的数据模型
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional, Any, TypeVar, Generic
|
from typing import List, Optional, Any, TypeVar, Generic, Dict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -46,6 +46,8 @@ class PaginationData(BaseModel, Generic[T]):
|
|||||||
|
|
||||||
|
|
||||||
# ============== 分页参数 ==============
|
# ============== 分页参数 ==============
|
||||||
|
|
||||||
|
class PaginationParams(BaseModel):
|
||||||
"""分页参数"""
|
"""分页参数"""
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
page: int = Field(default=1, ge=1, description="页码")
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
||||||
@@ -424,3 +426,20 @@ class RecruiterWithChannelsResponse(BaseModel):
|
|||||||
recruiter_id: str
|
recruiter_id: str
|
||||||
recruiter_name: str
|
recruiter_name: str
|
||||||
channels: List[RecruiterChannelBindingResponse]
|
channels: List[RecruiterChannelBindingResponse]
|
||||||
|
|
||||||
|
|
||||||
|
# 重建所有模型以解决前向引用问题
|
||||||
|
BaseResponse.model_rebuild()
|
||||||
|
PaginationData.model_rebuild()
|
||||||
|
CandidateResponse.model_rebuild()
|
||||||
|
CandidateListResponse.model_rebuild()
|
||||||
|
JobPositionResponse.model_rebuild()
|
||||||
|
JobPositionListResponse.model_rebuild()
|
||||||
|
RecruiterResponse.model_rebuild()
|
||||||
|
RecruiterListResponse.model_rebuild()
|
||||||
|
EvaluationSchemaResponse.model_rebuild()
|
||||||
|
EvaluationSchemaListResponse.model_rebuild()
|
||||||
|
NotificationChannelResponse.model_rebuild()
|
||||||
|
NotificationChannelListResponse.model_rebuild()
|
||||||
|
RecruiterChannelBindingResponse.model_rebuild()
|
||||||
|
RecruiterChannelBindingListResponse.model_rebuild()
|
||||||
|
|||||||
@@ -262,9 +262,11 @@ class CandidateMapper:
|
|||||||
count_stmt = count_stmt.where(*conditions)
|
count_stmt = count_stmt.where(*conditions)
|
||||||
total = session.execute(count_stmt).scalar()
|
total = session.execute(count_stmt).scalar()
|
||||||
|
|
||||||
# 分页查询,按LLM评分降序,再按创建时间倒序
|
# 分页查询,按LLM评分降序(NULL值在后),再按创建时间倒序
|
||||||
|
# 使用MySQL兼容的语法:先按是否为NULL排序,再按值排序
|
||||||
stmt = stmt.order_by(
|
stmt = stmt.order_by(
|
||||||
CandidateModel.llm_score.desc().nullslast(),
|
CandidateModel.llm_score.is_(None).asc(), # NULL值在后
|
||||||
|
CandidateModel.llm_score.desc(),
|
||||||
CandidateModel.created_at.desc()
|
CandidateModel.created_at.desc()
|
||||||
)
|
)
|
||||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||||
|
|||||||
@@ -330,8 +330,12 @@ class JobMapper:
|
|||||||
# 获取总数
|
# 获取总数
|
||||||
total = session.execute(count_stmt).scalar()
|
total = session.execute(count_stmt).scalar()
|
||||||
|
|
||||||
# 分页查询,按最后同步时间降序
|
# 分页查询,按最后同步时间降序(NULL值在后)
|
||||||
stmt = stmt.order_by(JobModel.last_sync_at.desc().nullslast())
|
# 使用MySQL兼容的语法:先按是否为NULL排序,再按值排序
|
||||||
|
stmt = stmt.order_by(
|
||||||
|
JobModel.last_sync_at.is_(None).asc(), # NULL值在后
|
||||||
|
JobModel.last_sync_at.desc()
|
||||||
|
)
|
||||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||||
|
|
||||||
results = session.execute(stmt).scalars().all()
|
results = session.execute(stmt).scalars().all()
|
||||||
|
|||||||
@@ -88,22 +88,22 @@ export const jobApi = {
|
|||||||
// 候选人管理 API
|
// 候选人管理 API
|
||||||
export const candidateApi = {
|
export const candidateApi = {
|
||||||
// 获取筛选通过的候选人
|
// 获取筛选通过的候选人
|
||||||
getFiltered: (params = {}) => api.get('/candidates/filtered', { params }),
|
getFiltered: (params = {}) => api.get('/api/candidates/filtered', { params }),
|
||||||
|
|
||||||
// 筛选候选人
|
// 筛选候选人
|
||||||
filter: (data) => api.post('/candidates/filter', data),
|
filter: (data) => api.post('/api/candidates/filter', data),
|
||||||
|
|
||||||
// 获取候选人详情
|
// 获取候选人详情
|
||||||
getDetail: (id) => api.get(`/candidates/${id}`),
|
getDetail: (id) => api.get(`/api/candidates/${id}`),
|
||||||
|
|
||||||
// 标记候选人筛选状态
|
// 标记候选人筛选状态
|
||||||
markFiltered: (data) => api.post('/candidates/mark-filtered', data),
|
markFiltered: (data) => api.post('/api/candidates/mark-filtered', data),
|
||||||
|
|
||||||
// 更新候选人评分
|
// 更新候选人评分
|
||||||
updateScore: (data) => api.post('/candidates/update-score', data),
|
updateScore: (data) => api.post('/api/candidates/update-score', data),
|
||||||
|
|
||||||
// 根据评分范围查询
|
// 根据评分范围查询
|
||||||
getByScoreRange: (params) => api.get('/candidates/by-score-range', { params })
|
getByScoreRange: (params) => api.get('/api/candidates/by-score-range', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定时任务管理 API
|
// 定时任务管理 API
|
||||||
@@ -139,6 +139,45 @@ export const schedulerApi = {
|
|||||||
stop: () => api.post('/api/scheduler/stop')
|
stop: () => api.post('/api/scheduler/stop')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知渠道管理 API
|
||||||
|
export const notificationChannelApi = {
|
||||||
|
// 获取渠道类型列表
|
||||||
|
getTypes: () => api.get('/api/notification-channels/types'),
|
||||||
|
|
||||||
|
// 获取通知渠道列表
|
||||||
|
getList: (params = {}) => api.get('/api/notification-channels', { params }),
|
||||||
|
|
||||||
|
// 获取通知渠道详情
|
||||||
|
getDetail: (id) => api.get(`/api/notification-channels/${id}`),
|
||||||
|
|
||||||
|
// 创建通知渠道
|
||||||
|
create: (data) => api.post('/api/notification-channels', data),
|
||||||
|
|
||||||
|
// 更新通知渠道
|
||||||
|
update: (id, data) => api.put(`/api/notification-channels/${id}`, data),
|
||||||
|
|
||||||
|
// 删除通知渠道
|
||||||
|
delete: (id) => api.delete(`/api/notification-channels/${id}`),
|
||||||
|
|
||||||
|
// 启用通知渠道
|
||||||
|
activate: (id) => api.post(`/api/notification-channels/${id}/activate`),
|
||||||
|
|
||||||
|
// 停用通知渠道
|
||||||
|
deactivate: (id) => api.post(`/api/notification-channels/${id}/deactivate`),
|
||||||
|
|
||||||
|
// 获取渠道绑定的招聘者列表
|
||||||
|
getChannelRecruiters: (id) => api.get(`/api/notification-channels/${id}/recruiters`),
|
||||||
|
|
||||||
|
// 绑定招聘者到渠道
|
||||||
|
bindRecruiter: (channelId, recruiterId, data) => api.post(`/api/notification-channels/${channelId}/recruiters/${recruiterId}`, data),
|
||||||
|
|
||||||
|
// 更新绑定配置
|
||||||
|
updateBinding: (channelId, recruiterId, data) => api.put(`/api/notification-channels/${channelId}/recruiters/${recruiterId}`, data),
|
||||||
|
|
||||||
|
// 解绑招聘者
|
||||||
|
unbindRecruiter: (channelId, recruiterId) => api.delete(`/api/notification-channels/${channelId}/recruiters/${recruiterId}`)
|
||||||
|
}
|
||||||
|
|
||||||
// 系统 API
|
// 系统 API
|
||||||
export const systemApi = {
|
export const systemApi = {
|
||||||
// 获取首页信息
|
// 获取首页信息
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ const routes = [
|
|||||||
name: 'Scheduler',
|
name: 'Scheduler',
|
||||||
component: () => import('@/views/Scheduler.vue'),
|
component: () => import('@/views/Scheduler.vue'),
|
||||||
meta: { title: '定时任务', icon: 'time' }
|
meta: { title: '定时任务', icon: 'time' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'notification-channels',
|
||||||
|
name: 'NotificationChannels',
|
||||||
|
component: () => import('@/views/NotificationChannels.vue'),
|
||||||
|
meta: { title: '通知渠道', icon: 'notification' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,679 @@
|
|||||||
|
<template>
|
||||||
|
<div class="channels-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">通知渠道管理</h2>
|
||||||
|
<t-button theme="primary" @click="showAddDialog">
|
||||||
|
<template #icon><t-icon name="add" /></template>
|
||||||
|
添加渠道
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<t-card class="search-card" :bordered="false">
|
||||||
|
<t-form layout="inline" :data="searchForm">
|
||||||
|
<t-form-item label="渠道类型">
|
||||||
|
<t-select v-model="searchForm.channel_type" placeholder="全部" clearable style="width: 150px">
|
||||||
|
<t-option label="企业微信" value="wechat_work" />
|
||||||
|
<t-option label="钉钉" value="dingtalk" />
|
||||||
|
<t-option label="飞书" value="feishu" />
|
||||||
|
<t-option label="邮件" value="email" />
|
||||||
|
<t-option label="通用Webhook" value="webhook" />
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-space>
|
||||||
|
<t-button theme="primary" @click="handleSearch">
|
||||||
|
<template #icon><t-icon name="search" /></template>
|
||||||
|
搜索
|
||||||
|
</t-button>
|
||||||
|
<t-button theme="default" @click="resetSearch">重置</t-button>
|
||||||
|
</t-space>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</t-card>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<t-card :bordered="false">
|
||||||
|
<t-table
|
||||||
|
:data="channelList"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="loading"
|
||||||
|
row-key="id"
|
||||||
|
stripe
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<t-pagination
|
||||||
|
v-model="pagination.page"
|
||||||
|
v-model:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-size-options="[10, 20, 50]"
|
||||||
|
@change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
:header="isEdit ? '编辑通知渠道' : '添加通知渠道'"
|
||||||
|
width="600px"
|
||||||
|
:confirm-btn="{ content: '确定', loading: submitting }"
|
||||||
|
:on-confirm="handleSubmit"
|
||||||
|
:on-close="() => dialogVisible = false"
|
||||||
|
>
|
||||||
|
<t-form ref="formRef" :data="form" :rules="rules" :label-width="120">
|
||||||
|
<t-form-item label="渠道名称" name="name">
|
||||||
|
<t-input v-model="form.name" placeholder="请输入渠道名称" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="渠道类型" name="channel_type">
|
||||||
|
<t-select v-model="form.channel_type" placeholder="请选择渠道类型" style="width: 100%" :disabled="isEdit">
|
||||||
|
<t-option label="企业微信" value="wechat_work" />
|
||||||
|
<t-option label="钉钉" value="dingtalk" />
|
||||||
|
<t-option label="飞书" value="feishu" />
|
||||||
|
<t-option label="邮件" value="email" />
|
||||||
|
<t-option label="通用Webhook" value="webhook" />
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="描述">
|
||||||
|
<t-textarea v-model="form.description" :rows="2" placeholder="请输入描述" />
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<!-- 飞书配置 -->
|
||||||
|
<template v-if="form.channel_type === 'feishu'">
|
||||||
|
<t-form-item label="Webhook地址" name="config.feishu_webhook">
|
||||||
|
<t-input v-model="form.config.feishu_webhook" placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..." />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="签名密钥">
|
||||||
|
<t-input v-model="form.config.feishu_secret" placeholder="可选,用于验证请求" />
|
||||||
|
</t-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 企业微信配置 -->
|
||||||
|
<template v-if="form.channel_type === 'wechat_work'">
|
||||||
|
<t-form-item label="Webhook地址" name="config.webhook_url">
|
||||||
|
<t-input v-model="form.config.webhook_url" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." />
|
||||||
|
</t-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 钉钉配置 -->
|
||||||
|
<template v-if="form.channel_type === 'dingtalk'">
|
||||||
|
<t-form-item label="Webhook地址" name="config.webhook_url">
|
||||||
|
<t-input v-model="form.config.webhook_url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=..." />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="签名密钥">
|
||||||
|
<t-input v-model="form.config.sign_secret" placeholder="可选,用于钉钉安全设置" />
|
||||||
|
</t-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 邮件配置 -->
|
||||||
|
<template v-if="form.channel_type === 'email'">
|
||||||
|
<t-form-item label="SMTP服务器" name="config.smtp_host">
|
||||||
|
<t-input v-model="form.config.smtp_host" placeholder="smtp.example.com" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="SMTP端口" name="config.smtp_port">
|
||||||
|
<t-input-number v-model="form.config.smtp_port" :min="1" :max="65535" placeholder="587" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="用户名" name="config.username">
|
||||||
|
<t-input v-model="form.config.username" placeholder="邮箱地址" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="密码" name="config.password">
|
||||||
|
<t-input v-model="form.config.password" type="password" placeholder="邮箱密码或授权码" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="发件人名称">
|
||||||
|
<t-input v-model="form.config.sender_name" placeholder="显示的发件人名称" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="使用TLS">
|
||||||
|
<t-switch v-model="form.config.use_tls" />
|
||||||
|
</t-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 通用Webhook配置 -->
|
||||||
|
<template v-if="form.channel_type === 'webhook'">
|
||||||
|
<t-form-item label="Webhook地址" name="config.webhook_url">
|
||||||
|
<t-input v-model="form.config.webhook_url" placeholder="https://..." />
|
||||||
|
</t-form-item>
|
||||||
|
</template>
|
||||||
|
</t-form>
|
||||||
|
</t-dialog>
|
||||||
|
|
||||||
|
<!-- 绑定招聘者对话框 -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="bindDialogVisible"
|
||||||
|
header="绑定招聘者"
|
||||||
|
width="600px"
|
||||||
|
:confirm-btn="{ content: '确定', loading: binding }"
|
||||||
|
:on-confirm="confirmBind"
|
||||||
|
:on-close="() => bindDialogVisible = false"
|
||||||
|
>
|
||||||
|
<t-form :data="bindForm" :label-width="120">
|
||||||
|
<t-form-item label="选择招聘者" name="recruiter_id">
|
||||||
|
<t-select v-model="bindForm.recruiter_id" placeholder="请选择招聘者" style="width: 100%">
|
||||||
|
<t-option
|
||||||
|
v-for="recruiter in recruiterList"
|
||||||
|
:key="recruiter.id"
|
||||||
|
:label="recruiter.name"
|
||||||
|
:value="recruiter.id"
|
||||||
|
/>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="启用通知">
|
||||||
|
<t-switch v-model="bindForm.is_enabled" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="新候选人通知">
|
||||||
|
<t-switch v-model="bindForm.notify_on_new_candidate" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="评价完成通知">
|
||||||
|
<t-switch v-model="bindForm.notify_on_evaluation" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="高分候选人通知">
|
||||||
|
<t-switch v-model="bindForm.notify_on_high_score" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="高分阈值" v-if="bindForm.notify_on_high_score">
|
||||||
|
<t-slider v-model="bindForm.high_score_threshold" :max="100" :step="1" />
|
||||||
|
<div class="score-value">{{ bindForm.high_score_threshold }} 分</div>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</t-dialog>
|
||||||
|
|
||||||
|
<!-- 管理绑定对话框 -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="manageBindDialogVisible"
|
||||||
|
header="管理绑定"
|
||||||
|
width="700px"
|
||||||
|
:footer="false"
|
||||||
|
>
|
||||||
|
<t-table
|
||||||
|
:data="currentBindings"
|
||||||
|
:columns="bindingColumns"
|
||||||
|
row-key="recruiter_id"
|
||||||
|
stripe
|
||||||
|
/>
|
||||||
|
</t-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, h } from 'vue'
|
||||||
|
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||||
|
import { notificationChannelApi, recruiterApi } from '@/api/api'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const channelList = ref([])
|
||||||
|
const recruiterList = ref([])
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{ colKey: 'name', title: '渠道名称', minWidth: 150 },
|
||||||
|
{
|
||||||
|
colKey: 'channel_type',
|
||||||
|
title: '类型',
|
||||||
|
width: 120,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
const typeMap = {
|
||||||
|
wechat_work: '企业微信',
|
||||||
|
dingtalk: '钉钉',
|
||||||
|
feishu: '飞书',
|
||||||
|
email: '邮件',
|
||||||
|
webhook: 'Webhook'
|
||||||
|
}
|
||||||
|
const themeMap = {
|
||||||
|
wechat_work: 'success',
|
||||||
|
dingtalk: 'primary',
|
||||||
|
feishu: 'warning',
|
||||||
|
email: 'default',
|
||||||
|
webhook: 'danger'
|
||||||
|
}
|
||||||
|
return h('t-tag', { theme: themeMap[row.channel_type] || 'default' }, typeMap[row.channel_type] || row.channel_type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'status',
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-tag', { theme: row.status === 'active' ? 'success' : 'default', size: 'small' },
|
||||||
|
row.status === 'active' ? '启用' : '停用')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ colKey: 'description', title: '描述', minWidth: 200, ellipsis: true },
|
||||||
|
{
|
||||||
|
colKey: 'recruiter_count',
|
||||||
|
title: '绑定招聘者',
|
||||||
|
width: 120,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
const count = row.recruiter_ids?.length || 0
|
||||||
|
return h('t-badge', { count: count, showZero: true }, `${count}个`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'operation',
|
||||||
|
title: '操作',
|
||||||
|
width: 280,
|
||||||
|
fixed: 'right',
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-space', { size: 'small' }, {
|
||||||
|
default: () => [
|
||||||
|
h('t-button', { size: 'small', onClick: () => handleEdit(row) }, '编辑'),
|
||||||
|
h('t-button', { size: 'small', theme: 'primary', onClick: () => handleBind(row) }, '绑定'),
|
||||||
|
h('t-button', { size: 'small', onClick: () => handleManageBind(row) }, '管理'),
|
||||||
|
h('t-button', {
|
||||||
|
size: 'small',
|
||||||
|
theme: row.status === 'active' ? 'warning' : 'success',
|
||||||
|
onClick: () => toggleStatus(row)
|
||||||
|
}, row.status === 'active' ? '停用' : '启用'),
|
||||||
|
h('t-button', { size: 'small', theme: 'danger', onClick: () => handleDelete(row) }, '删除')
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 绑定表格列
|
||||||
|
const bindingColumns = [
|
||||||
|
{ colKey: 'recruiter_id', title: '招聘者ID', minWidth: 200 },
|
||||||
|
{
|
||||||
|
colKey: 'is_enabled',
|
||||||
|
title: '启用',
|
||||||
|
width: 80,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-tag', { theme: row.is_enabled ? 'success' : 'default', size: 'small' },
|
||||||
|
row.is_enabled ? '是' : '否')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'notify_on_new_candidate',
|
||||||
|
title: '新候选人',
|
||||||
|
width: 100,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-tag', { theme: row.notify_on_new_candidate ? 'success' : 'default', size: 'small' },
|
||||||
|
row.notify_on_new_candidate ? '通知' : '关闭')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'notify_on_evaluation',
|
||||||
|
title: '评价完成',
|
||||||
|
width: 100,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-tag', { theme: row.notify_on_evaluation ? 'success' : 'default', size: 'small' },
|
||||||
|
row.notify_on_evaluation ? '通知' : '关闭')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'operation',
|
||||||
|
title: '操作',
|
||||||
|
width: 100,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-button', {
|
||||||
|
size: 'small',
|
||||||
|
theme: 'danger',
|
||||||
|
onClick: () => handleUnbind(row)
|
||||||
|
}, '解绑')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
channel_type: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
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: '',
|
||||||
|
channel_type: '',
|
||||||
|
description: '',
|
||||||
|
config: {
|
||||||
|
webhook_url: '',
|
||||||
|
secret: '',
|
||||||
|
access_token: '',
|
||||||
|
sign_secret: '',
|
||||||
|
smtp_host: '',
|
||||||
|
smtp_port: 587,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
use_tls: true,
|
||||||
|
sender_name: '',
|
||||||
|
feishu_webhook: '',
|
||||||
|
feishu_secret: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入渠道名称', trigger: 'blur' }],
|
||||||
|
channel_type: [{ required: true, message: '请选择渠道类型', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 绑定对话框
|
||||||
|
const bindDialogVisible = ref(false)
|
||||||
|
const binding = ref(false)
|
||||||
|
const currentChannel = ref(null)
|
||||||
|
const bindForm = reactive({
|
||||||
|
recruiter_id: '',
|
||||||
|
is_enabled: true,
|
||||||
|
notify_on_new_candidate: true,
|
||||||
|
notify_on_evaluation: true,
|
||||||
|
notify_on_high_score: false,
|
||||||
|
high_score_threshold: 80
|
||||||
|
})
|
||||||
|
|
||||||
|
// 管理绑定对话框
|
||||||
|
const manageBindDialogVisible = ref(false)
|
||||||
|
const currentBindings = ref([])
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await notificationChannelApi.getList({
|
||||||
|
channel_type: searchForm.channel_type,
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize
|
||||||
|
})
|
||||||
|
channelList.value = res.data?.items || []
|
||||||
|
pagination.total = res.data?.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('加载数据失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载招聘者列表
|
||||||
|
const loadRecruiters = async () => {
|
||||||
|
try {
|
||||||
|
const res = await recruiterApi.getList({ page_size: 100 })
|
||||||
|
recruiterList.value = res.data?.items || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载招聘者失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.channel_type = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handlePageChange = () => {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加
|
||||||
|
const showAddDialog = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
form.id = ''
|
||||||
|
form.name = ''
|
||||||
|
form.channel_type = ''
|
||||||
|
form.description = ''
|
||||||
|
resetConfig()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetConfig = () => {
|
||||||
|
form.config = {
|
||||||
|
webhook_url: '',
|
||||||
|
secret: '',
|
||||||
|
access_token: '',
|
||||||
|
sign_secret: '',
|
||||||
|
smtp_host: '',
|
||||||
|
smtp_port: 587,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
use_tls: true,
|
||||||
|
sender_name: '',
|
||||||
|
feishu_webhook: '',
|
||||||
|
feishu_secret: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
form.id = row.id
|
||||||
|
form.name = row.name
|
||||||
|
form.channel_type = row.channel_type
|
||||||
|
form.description = row.description
|
||||||
|
// 合并配置
|
||||||
|
form.config = { ...form.config, ...row.config }
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
// 构建提交数据
|
||||||
|
const submitData = {
|
||||||
|
name: form.name,
|
||||||
|
channel_type: form.channel_type,
|
||||||
|
description: form.description,
|
||||||
|
config: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型添加配置
|
||||||
|
if (form.channel_type === 'feishu') {
|
||||||
|
submitData.config = {
|
||||||
|
feishu_webhook: form.config.feishu_webhook,
|
||||||
|
feishu_secret: form.config.feishu_secret
|
||||||
|
}
|
||||||
|
} else if (form.channel_type === 'wechat_work' || form.channel_type === 'dingtalk' || form.channel_type === 'webhook') {
|
||||||
|
submitData.config = {
|
||||||
|
webhook_url: form.config.webhook_url,
|
||||||
|
sign_secret: form.config.sign_secret
|
||||||
|
}
|
||||||
|
} else if (form.channel_type === 'email') {
|
||||||
|
submitData.config = {
|
||||||
|
smtp_host: form.config.smtp_host,
|
||||||
|
smtp_port: form.config.smtp_port,
|
||||||
|
username: form.config.username,
|
||||||
|
password: form.config.password,
|
||||||
|
use_tls: form.config.use_tls,
|
||||||
|
sender_name: form.config.sender_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
await notificationChannelApi.update(form.id, submitData)
|
||||||
|
MessagePlugin.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await notificationChannelApi.create(submitData)
|
||||||
|
MessagePlugin.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('提交失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
const confirmDia = DialogPlugin.confirm({
|
||||||
|
header: '确认删除',
|
||||||
|
body: '确定要删除该通知渠道吗?',
|
||||||
|
confirmBtn: '确定',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await notificationChannelApi.delete(row.id)
|
||||||
|
MessagePlugin.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
confirmDia.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('删除失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换状态
|
||||||
|
const toggleStatus = async (row) => {
|
||||||
|
try {
|
||||||
|
if (row.status === 'active') {
|
||||||
|
await notificationChannelApi.deactivate(row.id)
|
||||||
|
MessagePlugin.success('已停用')
|
||||||
|
} else {
|
||||||
|
await notificationChannelApi.activate(row.id)
|
||||||
|
MessagePlugin.success('已启用')
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('操作失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定招聘者
|
||||||
|
const handleBind = (row) => {
|
||||||
|
currentChannel.value = row
|
||||||
|
bindForm.recruiter_id = ''
|
||||||
|
bindForm.is_enabled = true
|
||||||
|
bindForm.notify_on_new_candidate = true
|
||||||
|
bindForm.notify_on_evaluation = true
|
||||||
|
bindForm.notify_on_high_score = false
|
||||||
|
bindForm.high_score_threshold = 80
|
||||||
|
bindDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmBind = async () => {
|
||||||
|
if (!bindForm.recruiter_id) {
|
||||||
|
MessagePlugin.warning('请选择招聘者')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.value = true
|
||||||
|
try {
|
||||||
|
await notificationChannelApi.bindRecruiter(
|
||||||
|
currentChannel.value.id,
|
||||||
|
bindForm.recruiter_id,
|
||||||
|
{
|
||||||
|
is_enabled: bindForm.is_enabled,
|
||||||
|
notify_on_new_candidate: bindForm.notify_on_new_candidate,
|
||||||
|
notify_on_evaluation: bindForm.notify_on_evaluation,
|
||||||
|
notify_on_high_score: bindForm.notify_on_high_score,
|
||||||
|
high_score_threshold: bindForm.high_score_threshold
|
||||||
|
}
|
||||||
|
)
|
||||||
|
MessagePlugin.success('绑定成功')
|
||||||
|
bindDialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('绑定失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
binding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理绑定
|
||||||
|
const handleManageBind = async (row) => {
|
||||||
|
currentChannel.value = row
|
||||||
|
try {
|
||||||
|
const res = await notificationChannelApi.getChannelRecruiters(row.id)
|
||||||
|
currentBindings.value = res.data?.items || []
|
||||||
|
manageBindDialogVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('获取绑定列表失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解绑
|
||||||
|
const handleUnbind = async (row) => {
|
||||||
|
const confirmDia = DialogPlugin.confirm({
|
||||||
|
header: '确认解绑',
|
||||||
|
body: '确定要解绑该招聘者吗?',
|
||||||
|
confirmBtn: '确定',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await notificationChannelApi.unbindRecruiter(currentChannel.value.id, row.recruiter_id)
|
||||||
|
MessagePlugin.success('解绑成功')
|
||||||
|
// 刷新绑定列表
|
||||||
|
const res = await notificationChannelApi.getChannelRecruiters(currentChannel.value.id)
|
||||||
|
currentBindings.value = res.data?.items || []
|
||||||
|
loadData()
|
||||||
|
confirmDia.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('解绑失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
loadRecruiters()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.channels-page {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card :deep(.t-card__body) {
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-table) {
|
||||||
|
border-radius: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-card__body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -106,13 +106,67 @@
|
|||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
</t-dialog>
|
</t-dialog>
|
||||||
|
|
||||||
|
<!-- 通知渠道绑定对话框 -->
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="channelBindDialogVisible"
|
||||||
|
header="绑定通知渠道"
|
||||||
|
width="700px"
|
||||||
|
:footer="false"
|
||||||
|
>
|
||||||
|
<t-space direction="vertical" style="width: 100%">
|
||||||
|
<t-card title="已绑定渠道" :bordered="false" size="small">
|
||||||
|
<t-table
|
||||||
|
:data="currentRecruiterChannels"
|
||||||
|
:columns="boundChannelColumns"
|
||||||
|
row-key="channel_id"
|
||||||
|
stripe
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</t-card>
|
||||||
|
|
||||||
|
<t-card title="添加绑定" :bordered="false" size="small">
|
||||||
|
<t-form :data="channelBindForm" :label-width="100">
|
||||||
|
<t-form-item label="选择渠道">
|
||||||
|
<t-select v-model="channelBindForm.channel_id" placeholder="请选择通知渠道" style="width: 100%">
|
||||||
|
<t-option
|
||||||
|
v-for="channel in availableChannels"
|
||||||
|
:key="channel.id"
|
||||||
|
:label="channel.name"
|
||||||
|
:value="channel.id"
|
||||||
|
/>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="启用通知">
|
||||||
|
<t-switch v-model="channelBindForm.is_enabled" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="新候选人通知">
|
||||||
|
<t-switch v-model="channelBindForm.notify_on_new_candidate" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="评价完成通知">
|
||||||
|
<t-switch v-model="channelBindForm.notify_on_evaluation" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="高分候选人通知">
|
||||||
|
<t-switch v-model="channelBindForm.notify_on_high_score" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="高分阈值" v-if="channelBindForm.notify_on_high_score">
|
||||||
|
<t-slider v-model="channelBindForm.high_score_threshold" :max="100" :step="1" />
|
||||||
|
<div class="score-value">{{ channelBindForm.high_score_threshold }} 分</div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-button theme="primary" @click="confirmChannelBind" :loading="channelBinding">添加绑定</t-button>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</t-card>
|
||||||
|
</t-space>
|
||||||
|
</t-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, h } from 'vue'
|
import { ref, reactive, onMounted, h } from 'vue'
|
||||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||||
import { recruiterApi } from '@/api/api'
|
import { recruiterApi, notificationChannelApi } from '@/api/api'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -163,13 +217,14 @@ const columns = [
|
|||||||
{
|
{
|
||||||
colKey: 'operation',
|
colKey: 'operation',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 280,
|
width: 340,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
cell: (h, { row }) => {
|
cell: (h, { row }) => {
|
||||||
return h('t-space', {}, {
|
return h('t-space', { size: 'small' }, {
|
||||||
default: () => [
|
default: () => [
|
||||||
h('t-button', { size: 'small', onClick: () => handleEdit(row) }, '编辑'),
|
h('t-button', { size: 'small', onClick: () => handleEdit(row) }, '编辑'),
|
||||||
h('t-button', { size: 'small', theme: 'primary', onClick: () => handleSync(row) }, '同步'),
|
h('t-button', { size: 'small', theme: 'primary', onClick: () => handleSync(row) }, '同步'),
|
||||||
|
h('t-button', { size: 'small', onClick: () => handleChannelBind(row) }, '通知渠道'),
|
||||||
h('t-button', {
|
h('t-button', {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
theme: row.status === 'active' ? 'warning' : 'success',
|
theme: row.status === 'active' ? 'warning' : 'success',
|
||||||
@@ -182,6 +237,56 @@ const columns = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 已绑定渠道表格列
|
||||||
|
const boundChannelColumns = [
|
||||||
|
{ colKey: 'channel_name', title: '渠道名称', minWidth: 120 },
|
||||||
|
{
|
||||||
|
colKey: 'channel_type',
|
||||||
|
title: '类型',
|
||||||
|
width: 100,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
const typeMap = {
|
||||||
|
wechat_work: '企业微信',
|
||||||
|
dingtalk: '钉钉',
|
||||||
|
feishu: '飞书',
|
||||||
|
email: '邮件',
|
||||||
|
webhook: 'Webhook'
|
||||||
|
}
|
||||||
|
return typeMap[row.channel_type] || row.channel_type
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'is_enabled',
|
||||||
|
title: '启用',
|
||||||
|
width: 80,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-tag', { theme: row.is_enabled ? 'success' : 'default', size: 'small' },
|
||||||
|
row.is_enabled ? '是' : '否')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'notify_on_new_candidate',
|
||||||
|
title: '新候选人',
|
||||||
|
width: 90,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-tag', { theme: row.notify_on_new_candidate ? 'success' : 'default', size: 'small' },
|
||||||
|
row.notify_on_new_candidate ? '通知' : '关闭')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'operation',
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
cell: (h, { row }) => {
|
||||||
|
return h('t-button', {
|
||||||
|
size: 'small',
|
||||||
|
theme: 'danger',
|
||||||
|
onClick: () => handleUnbindChannel(row)
|
||||||
|
}, '解绑')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
source: ''
|
source: ''
|
||||||
@@ -226,6 +331,21 @@ const registerRules = {
|
|||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const registering = ref(false)
|
const registering = ref(false)
|
||||||
|
|
||||||
|
// 通知渠道绑定
|
||||||
|
const channelBindDialogVisible = ref(false)
|
||||||
|
const channelBinding = ref(false)
|
||||||
|
const currentRecruiter = ref(null)
|
||||||
|
const currentRecruiterChannels = ref([])
|
||||||
|
const availableChannels = ref([])
|
||||||
|
const channelBindForm = reactive({
|
||||||
|
channel_id: '',
|
||||||
|
is_enabled: true,
|
||||||
|
notify_on_new_candidate: true,
|
||||||
|
notify_on_evaluation: true,
|
||||||
|
notify_on_high_score: false,
|
||||||
|
high_score_threshold: 80
|
||||||
|
})
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -376,6 +496,103 @@ const formatTime = (time) => {
|
|||||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知渠道绑定相关方法
|
||||||
|
const handleChannelBind = async (row) => {
|
||||||
|
currentRecruiter.value = row
|
||||||
|
channelBindForm.channel_id = ''
|
||||||
|
channelBindForm.is_enabled = true
|
||||||
|
channelBindForm.notify_on_new_candidate = true
|
||||||
|
channelBindForm.notify_on_evaluation = true
|
||||||
|
channelBindForm.notify_on_high_score = false
|
||||||
|
channelBindForm.high_score_threshold = 80
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 加载招聘者已绑定的渠道
|
||||||
|
const bindRes = await notificationChannelApi.getChannelRecruiters(row.id)
|
||||||
|
currentRecruiterChannels.value = bindRes.data?.items || []
|
||||||
|
|
||||||
|
// 加载所有可用渠道
|
||||||
|
const channelRes = await notificationChannelApi.getList({ page_size: 100 })
|
||||||
|
const allChannels = channelRes.data?.items || []
|
||||||
|
|
||||||
|
// 过滤掉已绑定的渠道
|
||||||
|
const boundIds = currentRecruiterChannels.value.map(b => b.channel_id)
|
||||||
|
availableChannels.value = allChannels.filter(c => !boundIds.includes(c.id))
|
||||||
|
|
||||||
|
channelBindDialogVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('加载渠道信息失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmChannelBind = async () => {
|
||||||
|
if (!channelBindForm.channel_id) {
|
||||||
|
MessagePlugin.warning('请选择通知渠道')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelBinding.value = true
|
||||||
|
try {
|
||||||
|
await notificationChannelApi.bindRecruiter(
|
||||||
|
channelBindForm.channel_id,
|
||||||
|
currentRecruiter.value.id,
|
||||||
|
{
|
||||||
|
is_enabled: channelBindForm.is_enabled,
|
||||||
|
notify_on_new_candidate: channelBindForm.notify_on_new_candidate,
|
||||||
|
notify_on_evaluation: channelBindForm.notify_on_evaluation,
|
||||||
|
notify_on_high_score: channelBindForm.notify_on_high_score,
|
||||||
|
high_score_threshold: channelBindForm.high_score_threshold
|
||||||
|
}
|
||||||
|
)
|
||||||
|
MessagePlugin.success('绑定成功')
|
||||||
|
|
||||||
|
// 刷新已绑定列表
|
||||||
|
const bindRes = await notificationChannelApi.getChannelRecruiters(currentRecruiter.value.id)
|
||||||
|
currentRecruiterChannels.value = bindRes.data?.items || []
|
||||||
|
|
||||||
|
// 刷新可用渠道列表
|
||||||
|
const channelRes = await notificationChannelApi.getList({ page_size: 100 })
|
||||||
|
const allChannels = channelRes.data?.items || []
|
||||||
|
const boundIds = currentRecruiterChannels.value.map(b => b.channel_id)
|
||||||
|
availableChannels.value = allChannels.filter(c => !boundIds.includes(c.id))
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
channelBindForm.channel_id = ''
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('绑定失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
channelBinding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnbindChannel = async (row) => {
|
||||||
|
const confirmDia = DialogPlugin.confirm({
|
||||||
|
header: '确认解绑',
|
||||||
|
body: `确定要解绑渠道 "${row.channel_name}" 吗?`,
|
||||||
|
confirmBtn: '确定',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await notificationChannelApi.unbindRecruiter(row.channel_id, currentRecruiter.value.id)
|
||||||
|
MessagePlugin.success('解绑成功')
|
||||||
|
|
||||||
|
// 刷新已绑定列表
|
||||||
|
const bindRes = await notificationChannelApi.getChannelRecruiters(currentRecruiter.value.id)
|
||||||
|
currentRecruiterChannels.value = bindRes.data?.items || []
|
||||||
|
|
||||||
|
// 刷新可用渠道列表
|
||||||
|
const channelRes = await notificationChannelApi.getList({ page_size: 100 })
|
||||||
|
const allChannels = channelRes.data?.items || []
|
||||||
|
const boundIds = currentRecruiterChannels.value.map(b => b.channel_id)
|
||||||
|
availableChannels.value = allChannels.filter(c => !boundIds.includes(c.id))
|
||||||
|
|
||||||
|
confirmDia.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
MessagePlugin.error('解绑失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
})
|
})
|
||||||
@@ -432,4 +649,10 @@ onMounted(() => {
|
|||||||
:deep(.t-card__body) {
|
:deep(.t-card__body) {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user