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
|
||||
|
||||
|
||||
router = APIRouter(prefix="/candidates", tags=["候选人管理"])
|
||||
router = APIRouter(prefix="/api/candidates", tags=["候选人管理"])
|
||||
|
||||
|
||||
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])
|
||||
async def list_jobs(
|
||||
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="获取评价方案成功")
|
||||
except Exception as 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": {
|
||||
"recruiters": "/api/recruiters",
|
||||
"jobs": "/api/jobs",
|
||||
"candidates": "/candidates",
|
||||
"candidates": "/api/candidates",
|
||||
"scheduler": "/api/scheduler",
|
||||
"health": "/health"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ API共享Schema定义
|
||||
|
||||
集中定义所有API请求和响应的数据模型
|
||||
"""
|
||||
from typing import List, Optional, Any, TypeVar, Generic
|
||||
from typing import List, Optional, Any, TypeVar, Generic, Dict
|
||||
from datetime import datetime
|
||||
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_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
||||
@@ -424,3 +426,20 @@ class RecruiterWithChannelsResponse(BaseModel):
|
||||
recruiter_id: str
|
||||
recruiter_name: str
|
||||
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)
|
||||
total = session.execute(count_stmt).scalar()
|
||||
|
||||
# 分页查询,按LLM评分降序,再按创建时间倒序
|
||||
# 分页查询,按LLM评分降序(NULL值在后),再按创建时间倒序
|
||||
# 使用MySQL兼容的语法:先按是否为NULL排序,再按值排序
|
||||
stmt = stmt.order_by(
|
||||
CandidateModel.llm_score.desc().nullslast(),
|
||||
CandidateModel.llm_score.is_(None).asc(), # NULL值在后
|
||||
CandidateModel.llm_score.desc(),
|
||||
CandidateModel.created_at.desc()
|
||||
)
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
@@ -330,8 +330,12 @@ class JobMapper:
|
||||
# 获取总数
|
||||
total = session.execute(count_stmt).scalar()
|
||||
|
||||
# 分页查询,按最后同步时间降序
|
||||
stmt = stmt.order_by(JobModel.last_sync_at.desc().nullslast())
|
||||
# 分页查询,按最后同步时间降序(NULL值在后)
|
||||
# 使用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)
|
||||
|
||||
results = session.execute(stmt).scalars().all()
|
||||
|
||||
@@ -88,22 +88,22 @@ export const jobApi = {
|
||||
// 候选人管理 API
|
||||
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
|
||||
@@ -139,14 +139,53 @@ export const schedulerApi = {
|
||||
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
|
||||
export const systemApi = {
|
||||
// 获取首页信息
|
||||
getHome: () => api.get('/'),
|
||||
|
||||
|
||||
// 健康检查
|
||||
health: () => api.get('/health'),
|
||||
|
||||
|
||||
// 获取API状态
|
||||
getStatus: () => api.get('/api/status')
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ const routes = [
|
||||
name: 'Scheduler',
|
||||
component: () => import('@/views/Scheduler.vue'),
|
||||
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>
|
||||
@@ -98,21 +98,75 @@
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
<t-form-item label="WT Token" name="wt_token">
|
||||
<t-textarea
|
||||
v-model="registerForm.wt_token"
|
||||
<t-textarea
|
||||
v-model="registerForm.wt_token"
|
||||
:rows="3"
|
||||
placeholder="请输入WT Token,系统将自动获取账号信息"
|
||||
/>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
import { recruiterApi } from '@/api/api'
|
||||
import { recruiterApi, notificationChannelApi } from '@/api/api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -160,20 +214,21 @@ const columns = [
|
||||
width: 180,
|
||||
cell: (h, { row }) => formatTime(row.last_sync_at)
|
||||
},
|
||||
{
|
||||
colKey: 'operation',
|
||||
title: '操作',
|
||||
width: 280,
|
||||
{
|
||||
colKey: 'operation',
|
||||
title: '操作',
|
||||
width: 340,
|
||||
fixed: 'right',
|
||||
cell: (h, { row }) => {
|
||||
return h('t-space', {}, {
|
||||
return h('t-space', { size: 'small' }, {
|
||||
default: () => [
|
||||
h('t-button', { size: 'small', onClick: () => handleEdit(row) }, '编辑'),
|
||||
h('t-button', { size: 'small', theme: 'primary', onClick: () => handleSync(row) }, '同步'),
|
||||
h('t-button', {
|
||||
size: 'small',
|
||||
h('t-button', { size: 'small', onClick: () => handleChannelBind(row) }, '通知渠道'),
|
||||
h('t-button', {
|
||||
size: 'small',
|
||||
theme: row.status === 'active' ? 'warning' : 'success',
|
||||
onClick: () => toggleStatus(row)
|
||||
onClick: () => toggleStatus(row)
|
||||
}, row.status === 'active' ? '停用' : '启用'),
|
||||
h('t-button', { size: 'small', theme: 'danger', onClick: () => handleDelete(row) }, '删除')
|
||||
]
|
||||
@@ -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({
|
||||
source: ''
|
||||
@@ -226,6 +331,21 @@ const registerRules = {
|
||||
const submitting = 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 () => {
|
||||
loading.value = true
|
||||
@@ -376,6 +496,103 @@ const formatTime = (time) => {
|
||||
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(() => {
|
||||
loadData()
|
||||
})
|
||||
@@ -432,4 +649,10 @@ onMounted(() => {
|
||||
:deep(.t-card__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user