10 Commits

Author SHA1 Message Date
27942
bec9710643 haha 2026-03-06 10:05:25 +08:00
雷校云
6f35fde8c3 项目主题样式调整 2026-03-06 01:32:25 +08:00
雷校云
17e64c2c3f 项目主题样式调整 2026-03-06 01:22:22 +08:00
雷校云
18115cb30a 项目样式调整 2026-03-06 00:28:40 +08:00
雷校云
31461499ad 筛选管理弹窗组件修改 2026-03-05 23:27:36 +08:00
雷校云
2106a4a6ae 筛选管理页面接口对接 2026-03-05 20:53:54 +08:00
雷校云
dae0eeab69 复聊话术页面接口对接 2026-03-05 17:41:02 +08:00
雷校云
ff1f5b4cab 数据统计页面接口对接 2026-03-05 15:29:53 +08:00
雷校云
8b5bfa316a 联系方式页面增加搜索功能和导出表格功能 2026-03-04 15:18:33 +08:00
雷校云
a5defe482e 任务列表增加轮询调用 2026-03-03 15:48:42 +08:00
34 changed files with 3268 additions and 136 deletions

View File

@@ -77,3 +77,11 @@ export const ApiAccountsDelete = (id: string) => {
method: 'delete'
})
}
// 获取招聘筛选项(来自网站同步快照)
export const ApiRecruitFilterOptions = (accountId: string) => {
return request({
url: `/api/filters/options?account_id=${accountId}`,
method: 'get'
})
}

View File

@@ -5,9 +5,45 @@ import request from '@/utils/request'
* */
// 查询所有联系记录(含电话、微信号)
export const ApiContacts = (page: number, page_size: number) => {
export const ApiContacts = (data: any) => {
let start_date = ''
let end_date = ''
if (data.times && data.times.length) {
start_date = data.times[0]
end_date = data.times[1]
}
return request({
url: `/api/contacts?page=${page}&page_size=${page_size}`,
url: `/api/contacts?page=${data.pageNum}&page_size=${data.pageSize}&search=${data.search}&start_date=${start_date}&end_date=${end_date}`,
method: 'get'
})
}
// 导出联系记录为 Excel
export const ApiContactsExport = (data: any) => {
let start_date = ''
let end_date = ''
if (data.times && data.times.length) {
start_date = data.times[0]
end_date = data.times[1]
}
return request({
url: `/api/contacts/export?search=${data.search}&start_date=${start_date}&end_date=${end_date}`,
method: 'get'
})
}
// 获取总览统计数据
export const ApiStats = (data: any) => {
return request({
url: `/api/stats?period=${data.period}`,
method: 'get'
})
}
// 获取总览统计数据
export const ApiStatsDaily = (data: any) => {
return request({
url: `/api/stats/daily?days=${data.days}`,
method: 'get'
})
}

View File

@@ -0,0 +1,109 @@
import request from '@/utils/request'
/*
* 复聊管理
* */
// 获取复聊配置列表
export const ApiFollowupConfigs = () => {
return request({
url: `/api/followup-configs`,
method: 'get'
})
}
// 获取单个复聊配置
export const ApiFollowupConfigsId = (id: string) => {
return request({
url: `/api/followup-configs/${id}`,
method: 'get'
})
}
// 创建复聊配置
export const ApiFollowupConfigsAdd = (data: any) => {
const formData = new FormData()
formData.append('name', data.name)
formData.append('position', data.position)
formData.append('followup_days', data.followup_days)
formData.append('is_active', data.is_active)
return request({
url: `/api/followup-configs`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 更新复聊配置
export const ApiFollowupConfigsEditor = (data: any) => {
const formData = new FormData()
if (data.followup_days) formData.append('followup_days', data.followup_days)
formData.append('is_active', data.is_active)
return request({
url: `/api/followup-configs/${data.id}`,
method: 'put',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 删除复聊配置
export const ApiFollowupConfigsDelete = (id: string) => {
return request({
url: `/api/followup-configs/${id}`,
method: 'delete'
})
}
// 获取复聊话术列表
export const ApiFollowupScripts = (data: any) => {
return request({
url: `/api/followup-scripts?config_id=${data.config_id || ''}&day_number=${data.day_number || ''}`,
method: 'get'
})
}
// 创建复聊话术
export const ApiFollowupScriptsAdd = (data: any) => {
const formData = new FormData()
formData.append('config_id', data.config_id)
formData.append('day_number', data.day_number)
formData.append('order', data.order)
formData.append('content', data.content)
return request({
url: `/api/followup-scripts`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 更新复聊话术
export const ApiFollowupScriptsEditor = (data: any) => {
const formData = new FormData()
formData.append('content', data.content)
formData.append('order', data.order)
return request({
url: `/api/followup-scripts/${data.id}`,
method: 'put',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 删除复聊话术
export const ApiFollowupScriptsDelete = (id: string) => {
return request({
url: `/api/followup-scripts/${id}`,
method: 'delete'
})
}

View File

@@ -0,0 +1,71 @@
import request from '@/utils/request'
/*
* 筛选管理
* */
// 获取筛选配置列表
export const ApiFilters = () => {
return request({
url: `/api/filters`,
method: 'get'
})
}
// 获取单个复聊配置
export const ApiFiltersId = (id: string) => {
return request({
url: `/api/filters/${id}`,
method: 'get'
})
}
// 创建筛选配置
export const ApiFiltersAdd = (data: any) => {
const formData = new FormData()
formData.append('name', data.name)
formData.append('position_keywords', data.position_keywords)
formData.append('city', data.city)
formData.append('salary_min', data.salary_min)
formData.append('salary_max', data.salary_max)
formData.append('experience', data.experience)
formData.append('education', data.education)
formData.append('is_active', data.is_active)
return request({
url: `/api/filters`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 更新筛选配置
export const ApiFiltersEditor = (data: any) => {
const formData = new FormData()
if (data.name) formData.append('name', data.name)
if (data.position_keywords) formData.append('position_keywords', data.position_keywords)
if (data.city) formData.append('city', data.city)
if (data.salary_min) formData.append('salary_min', data.salary_min)
if (data.salary_max) formData.append('salary_max', data.salary_max)
if (data.experience) formData.append('experience', data.experience)
if (data.education) formData.append('education', data.education)
formData.append('is_active', data.is_active)
return request({
url: `/api/filters/${data.id}`,
method: 'put',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 删除筛选配置
export const ApiFiltersDelete = (id: string) => {
return request({
url: `/api/filters/${id}`,
method: 'delete'
})
}

View File

@@ -27,7 +27,12 @@ export const ApiTasksAdd = (data: any) => {
// formData.append('worker_id', data.worker_id)
// formData.append('account_name', data.account_name)
formData.append('id', data.boss_id)
formData.append('params', data.params)
const rawParams = data?.params
if (typeof rawParams === 'string') {
formData.append('params', rawParams)
} else {
formData.append('params', JSON.stringify(rawParams || {}))
}
return request({
url: `/api/tasks`,
method: 'post',

View File

@@ -24,13 +24,30 @@ defineProps({
width: 100%;
height: $navbar-height;
background-color: $sidebar-logo-background;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); // 添加底部分隔线
padding: 0 20px; // 增加内边距
.title {
flex-shrink: 0;
margin-left: 10px;
font-size: 14px;
font-weight: bold;
color: $sidebar-logo-text-color;
//margin-left: 10px;
font-size: 22px; // 增大字体,匹配示例模板
font-weight: 700; // 加粗
color: #60a5fa; // 使用亮蓝色,匹配示例模板
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); // 添加文字阴影
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&::after {
content: '专业自动化系统'; // 添加副标题
display: block;
font-size: 12px;
font-weight: normal;
opacity: 0.8;
margin-top: 2px;
color: $sidebar-logo-text-color;
}
}
}
</style>

View File

@@ -72,7 +72,8 @@ const menuThemeProps = computed(() => {
theme.value === 'dark' || sidebarColorScheme.value === SidebarColor.CLASSIC_BLUE
return {
backgroundColor: isDarkOrClassicBlue ? variables['menu-background'] : undefined,
// 对于深蓝色侧边栏配色,设置背景为透明以显示伪元素的渐变效果
backgroundColor: isDarkOrClassicBlue ? 'transparent' : undefined,
textColor: isDarkOrClassicBlue ? variables['menu-text'] : undefined,
activeTextColor: isDarkOrClassicBlue ? variables['menu-active-text'] : undefined
}

View File

@@ -184,9 +184,51 @@ html.dark {
}
}
// 深蓝色侧边栏配色(示例模板风格)- 优化对比度
html.sidebar-color-blue {
.el-menu-item:hover {
background-color: $menu-hover;
.el-menu {
// 普通菜单项文字 - 提高对比度
.el-menu-item,
.el-sub-menu__title {
color: #e2e8f0 !important;
&:hover {
background-color: rgba(255, 255, 255, 0.08) !important;
}
.menu-icon,
.el-icon {
color: #94a3b8 !important;
}
}
// 激活菜单项 - 更明显
.el-menu-item.is-active {
background-color: rgba(96, 165, 250, 0.2) !important;
border-left: 4px solid #60a5fa !important;
color: #60a5fa !important;
.menu-icon,
.el-icon {
color: #60a5fa !important;
}
}
// 子菜单标题悬停样式
.el-sub-menu__title:hover {
background-color: rgba(255, 255, 255, 0.08) !important;
}
// 父菜单激活状态
.el-sub-menu.has-active-child > .el-sub-menu__title {
color: #60a5fa !important;
background-color: rgba(96, 165, 250, 0.15) !important;
.menu-icon,
.el-icon {
color: #60a5fa !important;
}
}
}
}

View File

@@ -53,7 +53,8 @@ const { routes } = useLayoutMenu()
left: 0;
z-index: 999;
width: $sidebar-width;
background-color: $menu-background;
// 对于深蓝色配色方案,不设置背景色以显示渐变效果
background-color: transparent;
transition: width 0.28s;
&--collapsed {
@@ -63,7 +64,8 @@ const { routes } = useLayoutMenu()
.layout-sidebar {
position: relative;
height: 100%;
background-color: var(--menu-background);
// 使用 CSS 变量作为后备,但会被全局样式的伪元素覆盖
background-color: transparent;
transition: width 0.28s;
&.has-logo {

View File

@@ -324,6 +324,63 @@ export const constantRoutes: RouteRecordRaw[] = [
}
}
]
},
{
path: '/data',
name: 'Data',
component: Layout,
meta: {
title: '数据统计',
icon: 'setting'
},
children: [
{
path: 'dataStatistics',
name: 'DataStatistics',
component: () => import('@/views/DataStatistics/index.vue'),
meta: {
title: '数据统计'
}
}
]
},
{
path: '/reconciliation',
name: 'Reconciliation',
component: Layout,
meta: {
title: '复聊管理',
icon: 'setting'
},
children: [
{
path: 'reconciliationManagement',
name: 'ReconciliationManagement',
component: () => import('@/views/ReconciliationManagement/index.vue'),
meta: {
title: '复聊管理'
}
}
]
},
{
path: '/screening',
name: 'Screening',
component: Layout,
meta: {
title: '筛选管理',
icon: 'setting'
},
children: [
{
path: 'screeningManagement',
name: 'ScreeningManagement',
component: () => import('@/views/ScreeningManagement/index.vue'),
meta: {
title: '筛选管理'
}
}
]
}
// 注册平台登记
// {

View File

@@ -25,7 +25,7 @@ export const defaultSettings: AppSettings = {
// 语言
language: LanguageEnum.ZH_CN,
// 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss
themeColor: '#4080FF',
themeColor: '#1e40af',
// 是否显示水印
showWatermark: false,
// 水印内容
@@ -51,17 +51,18 @@ export const authConfig = {
enableTokenRefresh: true
} as const
// 主题色预设 - 经典配色方案
// 主题色预设 - 现代配色方案(基于示例模板风格)
// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
export const themeColorPresets = [
'#4080FF', // Arco Design 蓝 - 现代感强
'#1890FF', // Ant Design 蓝 - 经典商务
'#409EFF', // Element Plus 蓝 - 清新自然
'#FA8C16', // 活力橙 - 温暖友好
'#722ED1', // 优雅紫 - 高端大气
'#13C2C2', // 青色 - 科技
'#52C41A', // 成功绿 - 活力清新
'#F5222D', // 警示红 - 醒目强烈
'#2F54EB', // 深蓝 - 稳重专业
'#EB2F96' // 品红 - 时尚个性
'#1e40af', // Primary Blue - 示例模板主色
'#3b82f6', // Tailwind Blue - 现代科技感
'#2563eb', // Deep Blue - 深邃专业
'#0ea5e9', // Sky Blue - 清新明亮
'#6366f1', // Indigo - 优雅紫色
'#14b8a6', // Teal - 青绿高级
'#f97316', // Orange - 活力温暖
'#8b5cf6', // Violet - 时尚创意
'#ec4899', // Pink - 年轻潮流
'#10b981', // Emerald - 自然清新
'#06b6d4' // Cyan - 科技未来感
]

View File

@@ -1,45 +1,407 @@
$border: 1px solid var(--el-border-color-light);
// 全局变量
$border-radius-base: 12px; // 增大圆角,匹配示例模板
$border-radius-small: 8px;
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
$shadow-base: 0 4px 6px rgba(0, 0, 0, 0.05); // 调整阴影
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
$transition-base: all 0.3s ease; // 调整过渡时间
/* el-dialog */
.el-dialog {
border-radius: $border-radius-base;
box-shadow: $shadow-lg;
overflow: hidden;
.el-dialog__header {
padding: 15px 20px;
padding: 20px 24px;
margin: 0;
border-bottom: $border;
background: linear-gradient(135deg, #eff6ff 0%, #fff 100%);
.el-dialog__title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
}
.el-dialog__body {
padding: 20px;
padding: 24px;
}
.el-dialog__footer {
padding: 15px;
padding: 16px 24px;
border-top: $border;
background-color: #f9fafb;
}
}
/** el-drawer */
.el-drawer {
border-radius: $border-radius-base;
box-shadow: $shadow-lg;
.el-drawer__header {
padding: 15px 20px;
padding: 20px 24px;
margin: 0;
color: inherit;
border-bottom: $border;
background: linear-gradient(135deg, #eff6ff 0%, #fff 100%);
> :first-child {
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
}
.el-drawer__body {
padding: 20px;
padding: 24px;
}
.el-drawer__footer {
padding: 15px;
padding: 16px 24px;
border-top: $border;
background-color: #f9fafb;
}
}
// 抽屉和对话框底部按钮区域
.dialog-footer {
display: flex;
gap: 8px;
gap: 12px;
justify-content: flex-end;
.el-button {
min-width: 88px;
transition: $transition-base;
}
}
// el-card 样式优化
.el-card {
border-radius: $border-radius-base;
border: none;
box-shadow: $shadow-sm;
transition: $transition-base;
overflow: hidden;
&:hover {
box-shadow: $shadow-base;
}
.el-card__header {
padding: 18px 20px;
border-bottom: $border;
background-color: #f9fafb;
font-weight: 600;
color: #1f2937;
}
.el-card__body {
padding: 20px;
}
}
// el-table 样式优化
.el-table {
border-radius: $border-radius-small;
overflow: hidden;
th.el-table__cell {
background-color: #f3f4f6;
color: #374151;
font-weight: 600;
border-bottom: $border;
font-size: 14px;
}
td.el-table__cell {
border-bottom: $border;
transition: $transition-base;
}
tr {
&:hover {
background-color: #f9fafb;
}
}
.el-table__empty-block {
padding: 60px 0;
color: #9ca3af;
}
}
// el-button 样式优化
.el-button {
border-radius: $border-radius-small;
font-weight: 500;
transition: $transition-base;
&--primary {
background: linear-gradient(135deg, #1e40af, #1e3a8a);
&:hover,
&:focus {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(30, 64, 175, 0.3);
}
&:active {
transform: translateY(0);
}
}
&--success {
background: linear-gradient(135deg, #10b981, #059669);
&:hover,
&:focus {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.3);
}
}
&--danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
&:hover,
&:focus {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(239, 68, 68, 0.3);
}
}
&--warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
&:hover,
&:focus {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(245, 158, 11, 0.3);
}
}
}
// el-input 样式优化
.el-input {
.el-input__wrapper {
border-radius: $border-radius-small;
transition: $transition-base;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
&:hover {
box-shadow: 0 0 0 2px rgba(30, 64, 175, 0.1);
}
&.is-focus {
box-shadow: 0 0 0 2px rgba(30, 64, 175, 0.2);
}
}
}
// el-select 样式优化
.el-select {
.el-input__wrapper {
border-radius: $border-radius-small;
}
}
// el-pagination 样式优化
.el-pagination {
padding: 20px 0;
.el-pager li {
border-radius: $border-radius-small;
font-weight: 500;
transition: $transition-base;
&:hover {
transform: scale(1.05);
}
&.is-active {
background-color: #1e40af;
color: #fff;
}
}
.el-pagination__jump {
.el-input__wrapper {
border-radius: $border-radius-small;
}
}
}
// el-switch 样式优化
.el-switch {
&.is-checked {
.el-switch__core {
background-color: #10b981;
}
}
.el-switch__core {
border-radius: 10px;
transition: $transition-base;
}
}
// el-message 样式优化
.el-message {
border-radius: $border-radius-base;
box-shadow: $shadow-lg;
padding: 16px 20px;
font-weight: 500;
&--success {
background: linear-gradient(135deg, #ecfdf5 0%, #fff 100%);
border: 1px solid #d1fae5;
}
&--error {
background: linear-gradient(135deg, #fef2f2 0%, #fff 100%);
border: 1px solid #fee2e2;
}
&--warning {
background: linear-gradient(135deg, #fffbeb 0%, #fff 100%);
border: 1px solid #fef3c7;
}
&--info {
background: linear-gradient(135deg, #f3f4f6 0%, #fff 100%);
border: 1px solid #e5e7eb;
}
}
// el-tag 样式优化
.el-tag {
border-radius: $border-radius-small;
border: none;
font-weight: 500;
padding: 4px 12px;
}
// el-form 样式优化
.el-form {
.el-form-item__label {
font-weight: 500;
color: #374151;
}
.el-form-item__content {
.el-input,
.el-select,
.el-date-editor {
width: 100%;
}
}
}
// el-dropdown 样式优化
.el-dropdown-menu {
border-radius: $border-radius-base;
box-shadow: $shadow-base;
padding: 8px 0;
.el-dropdown-menu__item {
padding: 10px 16px;
transition: $transition-base;
&:hover {
background-color: #eff6ff;
color: #1d4ed8;
}
}
}
// el-progress 样式优化
.el-progress {
.el-progress__text {
font-weight: 600;
font-size: 14px;
}
&--line {
.el-progress-bar {
border-radius: 10px;
.el-progress-bar__outer {
border-radius: 10px;
background-color: #e5e7eb;
}
.el-progress-bar__inner {
border-radius: 10px;
background: linear-gradient(90deg, #1e40af, #3b82f6); // 使用渐变色
}
}
}
}
// el-radio & checkbox 样式优化
.el-radio,
.el-checkbox {
.el-radio__input.is-checked,
.el-checkbox__input.is-checked {
.el-radio__inner,
.el-checkbox__inner {
background-color: #1e40af;
border-color: #1e40af;
}
}
.el-radio__input.is-checked + .el-radio__label,
.el-checkbox__input.is-checked + .el-checkbox__label {
color: #1e40af;
font-weight: 500;
}
}
// el-alert 样式优化
.el-alert {
border-radius: $border-radius-base;
border: none;
padding: 16px 20px;
font-weight: 500;
}
// el-collapse 样式优化
.el-collapse {
border: none;
.el-collapse-item__header {
border: none;
margin-bottom: 8px;
border-radius: $border-radius-base;
padding: 14px 18px;
background-color: #f9fafb;
font-weight: 500;
color: #374151;
transition: $transition-base;
&:hover {
background-color: #f3f4f6;
}
&.is-active {
background-color: #eff6ff;
color: #1d4ed8;
}
}
.el-collapse-item__wrap {
border: none;
margin-bottom: 8px;
border-radius: $border-radius-base;
background-color: #fff;
}
}

View File

@@ -1,19 +1,28 @@
@use "./reset";
@use "./element-plus";
@use "./template-style"; // 示例模板风格
// Vxe Table
@use "./vxe-table";
@import url("./vxe-table.css");
// 全局变量
$border-radius-base: 12px; // 匹配示例模板的大圆角
$border-radius-small: 8px;
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
$shadow-base: 0 4px 6px rgba(0, 0, 0, 0.05); // 柔和阴影
$transition-base: all 0.3s ease;
.app-container {
padding: 15px;
padding: 25px; // 增加内边距,更加疏朗
}
// 进度条颜色
#nprogress .bar {
background-color: var(--el-color-primary);
height: 3px;
}
// 混合布局左侧菜单的hover样式
// 混合布局左侧菜单的 hover 样式
.layout-mix .layout__sidebar--left .el-menu {
.el-menu-item {
&:hover {
@@ -30,24 +39,38 @@
}
}
// 深色主题或深蓝色侧边栏配色下的左侧菜单hover样式
// 深色主题或深蓝色侧边栏配色下的左侧菜单 hover 样式
html.dark .layout-mix .layout__sidebar--left .el-menu,
html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu {
.el-menu-item {
&:hover {
// 深色背景使用CSS变量
// 深色背景:使用 CSS 变量
background-color: var(--menu-hover) !important;
}
}
.el-sub-menu__title {
&:hover {
// 深色背景使用CSS变量
// 深色背景:使用 CSS 变量
background-color: var(--menu-hover) !important;
}
}
}
// 深蓝色侧边栏渐变背景(示例模板风格)- 修复空白问题
html.sidebar-color-blue {
// 侧边栏容器应用渐变背景
.layout__sidebar,
.layout-sidebar {
background: linear-gradient(180deg, #1e293b, #0f172a) !important;
}
// 菜单容器设置为透明
.el-menu.el-menu--vertical {
background-color: transparent !important;
}
}
// 窄屏时隐藏菜单文字,只显示图标
.hideSidebar {
// Top布局和Mix布局的水平菜单
@@ -76,46 +99,95 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu {
// 全局搜索区域样式
.search-container {
padding: 18px 16px 0;
margin-bottom: 16px;
padding: 25px 20px; // 增加内边距
margin-bottom: 25px;
background-color: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
border-radius: $border-radius-base;
box-shadow: $shadow-base; // 使用更明显的阴影
.search-buttons {
margin-right: 0;
}
.el-form-item {
margin-bottom: 18px;
margin-bottom: 20px;
}
}
// 表格区域样式
.data-table {
margin-bottom: 16px;
margin-bottom: 25px;
background-color: var(--el-bg-color-overlay);
border-radius: $border-radius-base;
padding: 25px;
box-shadow: $shadow-base;
// 表格工具栏区域
&__toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
margin-bottom: 20px;
&--actions,
&--tools {
display: flex;
gap: 8px;
gap: 12px; // 增大按钮间距
}
}
// 表格内容区域
&__content {
margin: 8px 0;
margin: 0;
}
// 分页区域
.el-pagination {
justify-content: flex-end;
margin-top: 16px;
margin-top: 25px;
}
}
// 菜单样式优化 - 提高对比度和可见性
html.sidebar-color-blue {
.el-menu {
// 普通菜单项文字 - 提高对比度
.el-menu-item,
.el-sub-menu__title {
color: #e2e8f0 !important;
&:hover {
background-color: rgba(255, 255, 255, 0.08) !important;
}
.menu-icon,
.el-icon {
color: #94a3b8 !important;
}
}
// 激活菜单项 - 更明显
.el-menu-item.is-active {
background-color: rgba(96, 165, 250, 0.2) !important;
border-left: 4px solid #60a5fa !important;
color: #60a5fa !important;
.menu-icon,
.el-icon {
color: #60a5fa !important;
}
}
// 父菜单激活状态
.el-sub-menu.has-active-child > .el-sub-menu__title {
color: #60a5fa !important;
background-color: rgba(96, 165, 250, 0.15) !important;
.menu-icon,
.el-icon {
color: #60a5fa !important;
}
}
}
}

View File

@@ -0,0 +1,300 @@
/**
* 示例模板风格卡片样式
* 基于页面模板的现代简约设计风格
*/
// 全局变量
$card-border-radius: 12px;
$card-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
$card-hover-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
$transition-base: all 0.3s ease;
// 通用卡片容器
.page-card {
background: #ffffff;
border-radius: $card-border-radius;
padding: 25px;
margin-bottom: 25px;
box-shadow: $card-shadow;
border: 1px solid #e2e8f0;
transition: $transition-base;
&:hover {
box-shadow: $card-hover-shadow;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #e2e8f0;
h3 {
color: #1e40af; // 主题色
font-size: 18px;
font-weight: 600;
margin: 0;
}
}
}
// 统计卡片 - 仿示例模板的 stats-grid
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin-bottom: 30px;
.stat-card {
background: #ffffff;
padding: 20px;
border-radius: $card-border-radius;
border: 1px solid #e2e8f0;
transition: $transition-base;
&:hover {
transform: translateY(-5px);
box-shadow: $card-hover-shadow;
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
&.icon-blue {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
}
&.icon-green {
background: linear-gradient(135deg, #10b981, #059669);
}
&.icon-orange {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
&.icon-purple {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
}
}
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #1e293b;
margin-bottom: 5px;
}
.stat-label {
color: #64748b;
font-size: 14px;
}
}
}
// 按钮样式 - 仿示例模板的渐变效果
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: $transition-base;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
&-primary {
background: linear-gradient(135deg, #1e40af, #1e3a8a);
color: white;
&:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(30, 64, 175, 0.3);
}
}
&-success {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
&:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.3);
}
}
&-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
&:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(239, 68, 68, 0.3);
}
}
&-warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
&:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(245, 158, 11, 0.3);
}
}
&-secondary {
background: #f8fafc;
color: #1e293b;
border: 1px solid #e2e8f0;
&:hover {
background: #e2e8f0;
}
}
&-sm {
padding: 6px 12px;
font-size: 12px;
}
}
// 表格样式 - 仿示例模板
.data-table-modern {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
th {
background: #f8fafc;
padding: 15px;
text-align: left;
font-weight: 600;
color: #64748b;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
td {
padding: 15px;
border-bottom: 1px solid #e2e8f0;
}
tr {
&:hover {
background: #f8fafc;
}
&.selected {
background: rgba(30, 64, 175, 0.05);
}
}
}
// 进度条样式
.progress-bar-modern {
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #1e40af, #3b82f6);
border-radius: 4px;
transition: width 0.3s ease;
}
}
// 状态标签
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-align: center;
&.status-running {
background: #d1fae5;
color: #065f46;
}
&.status-paused {
background: #fef3c7;
color: #92400e;
}
&.status-stopped {
background: #fee2e2;
color: #991b1b;
}
}
// 状态指示点
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
&.status-online {
background-color: #10b981;
box-shadow: 0 0 10px #10b981;
}
&.status-offline {
background-color: #ef4444;
}
}
// 动画效果
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.pulse {
animation: pulse 2s infinite;
}
// 批量操作栏
.batch-controls {
background: #ffffff;
border-radius: $card-border-radius;
padding: 20px;
margin-top: 20px;
border: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
.batch-info {
display: flex;
align-items: center;
gap: 15px;
}
.batch-actions {
display: flex;
gap: 15px;
}
}

View File

@@ -2,49 +2,54 @@
$colors: (
"primary": (
// 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor
"base": #4080ff,
"base": #1e40af,
),
"success": (
"base": #23c343,
"base": #10b981,
),
"warning": (
"base": #ff9a2e,
"base": #f59e0b,
),
"danger": (
"base": #f76560,
"base": #ef4444,
),
"info": (
"base": #a9aeb8,
"base": #0ea5e9,
),
),
$bg-color: (
"page": #f5f8fd,
"page": #f8fafc,
)
);
/** 全局SCSS变量 */
:root {
--menu-background: #fff; // 菜单背景色
--menu-text: #212121; // 菜单文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定
--menu-active-text: var(
--el-menu-active-color
); // 菜单激活文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定
--menu-hover: #e6f4ff; // 菜单悬停背景色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定
--sidebar-logo-background: #f5f5f5; // 侧边栏 Logo 背景色
--sidebar-logo-text-color: #333; // 侧边栏 Logo 文字颜色
--menu-background: #fff;
--menu-text: #1f2937;
--menu-active-text: var(--el-menu-active-color);
--menu-hover: #eff6ff;
--sidebar-logo-background: #f8fafc;
--sidebar-logo-text-color: #1e293b;
// 卡片和边框样式
--card-border-radius: 12px;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
--border-color: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #64748b;
}
/** 浅色主题-深蓝色侧边栏配色 */
/** 浅色主题 - 深蓝色侧边栏配色(仿示例模板风格) */
html.sidebar-color-blue {
--menu-background: #304156; // 菜单背景色
--menu-text: #bfcbd9; // 菜单文字颜色
--menu-active-text: var(--el-menu-active-color); // 菜单激活文字颜色
--menu-hover: #263445; // 菜单悬停背景色
--sidebar-logo-background: #2d3748; // 侧边栏 Logo 背景色
--sidebar-logo-text-color: #fff; // 侧边栏 Logo 文字颜色
--menu-background: linear-gradient(180deg, #1e293b, #0f172a);
--menu-text: #94a3b8;
--menu-active-text: #60a5fa;
--menu-hover: rgba(255, 255, 255, 0.08);
--sidebar-logo-background: #0f172a;
--sidebar-logo-text-color: #f1f5f9;
// 激活项的左边框颜色
--menu-active-border: #60a5fa;
}
/** 暗黑主题 */
@@ -87,7 +92,7 @@ $menu-hover: var(--menu-hover); // 菜单悬停背景色
$sidebar-logo-background: var(--sidebar-logo-background); // 侧边栏 Logo 背景色
$sidebar-logo-text-color: var(--sidebar-logo-text-color); // 侧边栏 Logo 文字颜色
$sidebar-width: 210px; // 侧边栏宽度
$sidebar-width: 260px; // 侧边栏宽度
$sidebar-width-collapsed: 54px; // 侧边栏收缩宽度
$navbar-height: 50px; // 导航栏高度
$navbar-height: 100px; // 导航栏高度
$tags-view-height: 34px; // TagsView 高度

View File

@@ -187,11 +187,19 @@ export function deepCloneByJSON(obj: any) {
}
/**
* 将 ISO 8601 时间格式转换为年月日时分秒格式
* 将 ISO 8601 时间格式转换为指定格式
* @param isoString ISO 8601 格式的时间字符串,如 "2026-02-28T14:10:51.966269"
* @returns 格式化后的时间字符串,如 "2026-02-28 14:10:51"
* @param format 格式化模板,默认 'YYYY-MM-DD HH:mm:ss'
* @returns 格式化后的时间字符串
*
* @example
* formatISOToDateTime('2026-02-28T14:10:51.966269') // '2026-02-28 14:10:51'
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'YYYY-MM-DD') // '2026-02-28'
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'YYYY/MM/DD HH:mm') // '2026/02/28 14:10'
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'MM-DD HH:mm') // '02-28 14:10'
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'YYYY 年 MM 月 DD 日') // '2026 年 02 月 28 日'
*/
export function formatISOToDateTime(isoString: string): string {
export function formatISOToDateTime(isoString: string, format = 'YYYY-MM-DD HH:mm:ss'): string {
if (!isoString) return ''
try {
@@ -210,7 +218,14 @@ export function formatISOToDateTime(isoString: string): string {
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
// 替换格式化模板中的占位符
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
} catch (error) {
console.error('日期格式化错误:', error)
return isoString

View File

@@ -0,0 +1,133 @@
<template>
<div class="recruit-task-form">
<el-alert
v-if="syncedAt"
type="info"
:closable="false"
show-icon
style="margin-bottom: 12px"
:title="`筛选项同步时间:${syncedAt}`"
/>
<el-alert
v-else
type="warning"
:closable="false"
show-icon
style="margin-bottom: 12px"
title="当前账号暂无筛选项,请先启动客户端同步筛选项"
/>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="auto" label-position="top">
<el-form-item label="筛选条件(可多选)" prop="selected_filters">
<el-select
v-model="formData.selected_filters"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
:loading="optionsLoading"
placeholder="请选择筛选条件"
style="width: 100%"
>
<el-option
v-for="item in filterOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="招聘人数" prop="greet_target">
<el-input-number
v-model="formData.greet_target"
:min="1"
:max="500"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { ApiRecruitFilterOptions } from '@/api/BoosAccountManagement'
const props = defineProps({
accountId: {
type: String,
default: ''
}
})
const formRef = ref()
const optionsLoading = ref(false)
const filterOptions = ref<string[]>([])
const syncedAt = ref('')
const formData = reactive({
selected_filters: [] as string[],
greet_target: 20
})
const formRules = reactive<any>({
selected_filters: [{ required: true, message: '请至少选择一个筛选条件', trigger: 'change' }],
greet_target: [{ required: true, message: '请输入招聘人数', trigger: 'blur' }]
})
const fetchOptions = () => {
if (!props.accountId) return
optionsLoading.value = true
ApiRecruitFilterOptions(props.accountId)
.then((res: any) => {
filterOptions.value = (res.data?.flat_options || []) as string[]
syncedAt.value = res.data?.synced_at || ''
})
.finally(() => {
optionsLoading.value = false
})
}
onMounted(() => {
fetchOptions()
})
const getForm = () => {
return {
selected_filters: [...formData.selected_filters],
greet_target: Number(formData.greet_target) || 20
}
}
const submit = (): Promise<boolean> => {
return new Promise((resolve, reject) => {
if (filterOptions.value.length === 0) {
ElMessage.error('暂无可选筛选项,请先启动客户端同步筛选项')
reject(false)
return
}
formRef.value?.validate((valid: boolean) => {
if (valid) {
resolve(true)
} else {
ElMessage.error('请完善招聘参数')
reject(false)
}
})
})
}
defineExpose({
submit,
getForm
})
</script>
<style scoped lang="scss">
.recruit-task-form {
width: 100%;
padding-right: 12px;
}
</style>

View File

@@ -14,7 +14,7 @@
>
<el-table-column label="任务类型" prop="task_type">
<template #default="scope">
<span>{{ scope.row.task_type === 'check_login' ? '检查登录' : '招聘' }}</span>
<span>{{ taskTypeLabel(scope.row.task_type) }}</span>
</template>
</el-table-column>
<el-table-column label="指定电脑" prop="worker_id" />
@@ -30,6 +30,10 @@
<template #default="scope">
<el-tag v-if="scope.row.status === 'success'" type="success">成功</el-tag>
<el-tag v-else-if="scope.row.status === 'failed'" type="danger">失败</el-tag>
<el-tag v-else-if="scope.row.status === 'pending'" type="primary">已创建</el-tag>
<el-tag v-else-if="scope.row.status === 'dispatched'" type="primary">已派发</el-tag>
<el-tag v-else-if="scope.row.status === 'running'" type="primary">执行中</el-tag>
<el-tag v-else-if="scope.row.status === 'cancelled'" type="info">已取消</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_at">
@@ -54,7 +58,6 @@
</template>
<script setup lang="ts">
// 表格数据
import { ApiTasks } from '@/api/TaskManagement'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
import { createTimer } from '@/utils/TimerManager'
@@ -72,22 +75,33 @@ const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10
})
// 获取数据
const taskTypeLabel = (taskType: string) => {
if (taskType === 'check_login') return '检查登录'
if (taskType === 'boss_recruit') return '招聘'
if (taskType === 'boss_reply') return '回复'
return taskType || '--'
}
function fetchData() {
ApiTasks(props.bossId, queryParams.pageNum, queryParams.pageSize).then((res: any) => {
roleList.value = res.data.results
total.value = res.data.total
})
}
defineExpose({
fetchData
})
onMounted(() => {
startTimer(fetchData, 5000)
})
onUnmounted(() => {
stopTimer()
})
</script>
<style scoped lang="scss"></style>

View File

@@ -139,6 +139,13 @@
</el-table-column>
<el-table-column fixed="right" label="操作" width="300">
<template #default="scope">
<el-button
type="warning"
size="small"
@click="onApiTasksAdd(scope.row.id, 'boss_reply', {})"
>
回复
</el-button>
<!-- <el-button-->
<!-- type="primary"-->
<!-- size="small"-->
@@ -160,14 +167,14 @@
<el-button
type="primary"
size="small"
@click="onApiTasksAdd(scope.row.id, 'check_login', '')"
@click="onApiTasksAdd(scope.row.id, 'check_login', {})"
>
检查登录
</el-button>
<el-button
type="success"
size="small"
@click="onApiTasksAdd(scope.row.id, 'boss_recruit', '')"
@click="openRecruitDialog(scope.row.id)"
>
招聘
</el-button>
@@ -217,6 +224,7 @@
import { RolePageVO } from '@/api/system/role-api'
import { functionDialogBox } from '@/utils/functionDialogBox'
import BoosAccountForm from './components/BoosAccountForm.vue'
import RecruitTaskForm from './components/RecruitTaskForm.vue'
import { BusinessEditApplication } from '@/api/calibration/applicationForSealApproval'
import { ApiAccounts, ApiAccountsAdd, ApiAccountsDelete } from '@/api/BoosAccountManagement'
import ViewTheTaskListDetails from './components/ViewTheTaskListDetails.vue'
@@ -370,7 +378,23 @@ const onUserDeleteDepartment = (id: string) => {
// )
// }
const onApiTasksAdd = (id: string, task_type: string, params: string) => {
const openRecruitDialog = (id: string) => {
functionDialogBox(
RecruitTaskForm,
{
accountId: id
},
{
title: '招聘参数',
width: '620',
ok(value: any) {
onApiTasksAdd(id, 'boss_recruit', value || {})
}
}
)
}
const onApiTasksAdd = (id: string, task_type: string, params: any = {}) => {
// functionDialogBox(
// TaskForm,
// {},
@@ -387,7 +411,7 @@ const onApiTasksAdd = (id: string, task_type: string, params: string) => {
// }
// }
// )
loading.value = true
ApiTasksAdd({ params, task_type, boss_id: id })
.then((res: any) => {
ElMessage.success(res.msg)

View File

@@ -8,10 +8,10 @@
label-position="top"
>
<el-form-item label="任务类型" prop="task_type">
<!-- <el-input v-model="formData.task_type" placeholder="请输入" />-->
<el-select v-model="formData.task_type" placeholder="请选择">
<el-option label="检查登录" value="check_login" />
<el-option label="招聘" value="boss_recruit" />
<el-option label="回复" value="boss_reply" />
</el-select>
</el-form-item>
<el-form-item label="任务参数" prop="params">
@@ -33,7 +33,6 @@ const props = defineProps({
})
const formRef = ref()
// 表单数据
const formData = reactive<any>({
task_type: '',
worker_id: '',
@@ -56,7 +55,6 @@ const getForm = () => {
const setFormData = (data: any) => {
if (data && Object.keys(data).length > 0) {
const data1 = deepCloneByJSON(data)
Object.assign(formData, data1)
}
}
@@ -73,6 +71,7 @@ const submit = (): Promise<boolean> => {
})
})
}
defineExpose({
submit,
getForm
@@ -108,3 +107,4 @@ defineExpose({
gap: 20px;
}
</style>

View File

@@ -1,47 +1,42 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<!-- <div class="search-container">-->
<!-- <el-form ref="queryFormRef" :model="queryParams" :inline="true">-->
<!-- <el-form-item prop="seal_type" label="用印用途">-->
<!-- <el-input-->
<!-- v-model="queryParams.seal_type"-->
<!-- placeholder="请输入"-->
<!-- clearable-->
<!-- @keyup.enter="handleQuery"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- <el-form-item prop="CaseNumber" label="合同编号">-->
<!-- <el-input-->
<!-- v-model="queryParams.CaseNumber"-->
<!-- placeholder="请输入"-->
<!-- clearable-->
<!-- @keyup.enter="handleQuery"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间" prop="times">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.times"-->
<!-- type="daterange"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择创建时间"-->
<!-- range-separator="至"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="结束时间"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- <el-form-item class="search-buttons">-->
<!-- <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>-->
<!-- <el-button icon="refresh" @click="handleResetQuery">重置</el-button>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </div>-->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item prop="search" label="姓名">
<el-input
v-model="queryParams.search"
placeholder="请输入"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="创建时间" prop="times">
<el-date-picker
v-model="queryParams.times"
type="daterange"
value-format="YYYY-MM-DD"
placeholder="请选择创建时间"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<!-- <div class="data-table__toolbar">-->
<!-- <div class="data-table__toolbar&#45;&#45;actions">-->
<!-- <el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>-->
<!-- </div>-->
<!-- </div>-->
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<!-- <el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>-->
</div>
<div class="data-table__toolbar--actions">
<el-button type="success" icon="Download" @click="onApiContactsExport()">导出</el-button>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
@@ -80,6 +75,11 @@
{{ formatISOToDateTime(scope.row?.contacted_at || '') }}
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_at">
<template #default="scope">
{{ formatISOToDateTime(scope.row?.created_at || '') }}
</template>
</el-table-column>
<!-- <el-table-column fixed="right" label="操作" width="140">-->
<!-- <template #default="scope">-->
<!-- <el-button-->
@@ -116,7 +116,7 @@
</template>
<script setup lang="ts">
import { ApiContacts } from '@/api/ContactInformation'
import { ApiContacts, ApiContactsExport } from '@/api/ContactInformation'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
defineOptions({
@@ -124,7 +124,7 @@ defineOptions({
inheritAttrs: false
})
// const queryFormRef = ref()
const queryFormRef = ref()
const loading = ref(false)
const ids = ref<number[]>([])
@@ -134,8 +134,7 @@ const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
times: [],
seal_type: '',
CaseNumber: ''
search: ''
})
// 表格数据
@@ -150,7 +149,7 @@ const roleList = ref<any[]>()
// 获取数据
function fetchData() {
loading.value = true
ApiContacts(queryParams.pageNum, queryParams.pageSize)
ApiContacts(queryParams)
.then((res: any) => {
roleList.value = res.data.results
total.value = res.data.total
@@ -167,17 +166,31 @@ function handleQuery() {
}
// // 重置查询
// function handleResetQuery() {
// if (queryFormRef.value) queryFormRef.value?.resetFields()
// queryParams.pageNum = 1
// fetchData()
// }
function handleResetQuery() {
if (queryFormRef.value) queryFormRef.value?.resetFields()
queryParams.pageNum = 1
fetchData()
}
// 行复选框选中
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id)
}
const onApiContactsExport = () => {
ApiContactsExport(queryParams).then((res: any) => {
if (res.data.download_url) {
const link = document.createElement('a')
link.href = res.data.download_url
link.download = res.data.filename || '联系人信息.xlsx'
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
})
}
// // 打开角色弹窗
// function handleOpenDialog(data: any = null) {
// dialog.visible = true

View File

@@ -0,0 +1,199 @@
<template>
<div class="app-container">
<div class="chart-header">
<div class="chart-title">每日数据明细</div>
<el-radio-group v-model="queryParams.days" size="small" @change="handleTimeChange">
<el-radio-button v-for="item in timeOptions" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<!-- <el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>-->
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="roleList"
highlight-current-row
border
class="data-table__content"
>
<el-table-column label="联系人数" prop="contacts" />
<el-table-column label="回复人数" prop="replied" />
<el-table-column label="微信相关数量" prop="wechat"></el-table-column>
<el-table-column label="回复率" prop="reply_rate" />
<el-table-column label="日期时间" prop="date">
<template #default="scope">
{{ formatISOToDateTime(scope.row?.date || '') }}
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchData"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ApiStatsDaily } from '@/api/ContactInformation'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
import { ref } from 'vue'
defineOptions({
name: 'Role',
inheritAttrs: false
})
// const queryFormRef = ref()
const loading = ref(false)
const total = ref(0)
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
days: '1'
})
// 表格数据
const roleList = ref<any[]>()
const timeOptions = [
{ label: '今日', value: '1' },
{ label: '近 7 天', value: '7' },
{ label: '近 30 天', value: '30' }
]
// // 弹窗
// const dialog = reactive({
// title: '',
// visible: false
// })
// 获取数据
function fetchData() {
loading.value = true
ApiStatsDaily(queryParams)
.then((res: any) => {
roleList.value = res.data
// total.value = res.data.total
})
.finally(() => {
loading.value = false
})
}
// 查询(重置页码后获取数据)
function handleQuery() {
queryParams.pageNum = 1
fetchData()
}
// // 重置查询
// function handleResetQuery() {
// if (queryFormRef.value) queryFormRef.value?.resetFields()
// queryParams.pageNum = 1
// fetchData()
// }
const handleTimeChange = () => {
handleQuery()
}
onMounted(() => {
handleQuery()
})
</script>
<style lang="scss" scoped>
.headline-statistics {
display: flex;
gap: 20px;
margin-bottom: 16px;
height: 140px;
}
::v-deep(.statistics-box) {
display: flex;
//justify-content: center;
align-items: center;
color: #fff;
font-size: 30px;
gap: 20px;
.statistics-box-img {
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 15px;
}
.img-bg1 {
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
}
.img-bg2 {
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
}
.img-bg3 {
background: linear-gradient(135deg, #56bafd 0%, #16e9fd 100%);
}
.img-bg4 {
background: linear-gradient(135deg, #51eb90 0%, #49f7d2 100%);
}
.statistics-box-text {
//flex: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
color: #adb0b3;
& > span:nth-child(1) {
font-size: 14px;
}
& > span:nth-child(2) {
font-size: 26px;
color: #000;
font-weight: 700;
}
& > span:nth-child(3) {
font-size: 12px;
}
}
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-title {
font-size: 20px;
font-weight: 600;
color: #303133;
}
.time-filter {
display: flex;
gap: 8px;
:deep(.el-button) {
padding: 5px 12px;
font-size: 13px;
&.el-button--primary {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="chart-container">
<div ref="chartRef" class="trend-chart"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import * as echarts from 'echarts'
import type { EChartsOption } from 'echarts'
import { ApiStatsDaily } from '@/api/ContactInformation'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
interface Props {
height?: string
}
withDefaults(defineProps<Props>(), {
height: '350px'
})
const data = ref<any>({
xAxis: [] as string[],
series: [
{
name: '联系人数',
data: []
},
{
name: '已回复',
data: []
},
{
name: '微信交换',
data: []
}
]
})
const chartRef = ref<HTMLElement | null>(null)
let chartInstance: echarts.ECharts | null = null
// 颜色配置
const seriesColors = ['#5B9BFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
// 初始化图表
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.value.xAxis,
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
color: '#D7DDE3'
}
},
axisLabel: {
color: '#909399',
margin: 20,
rotate: 45,
interval: 0,
fontSize: 12
}
},
yAxis: {
type: 'value',
min: 0,
max: 1,
interval: 0.2,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
lineStyle: {
color: '#E8E8E8',
type: 'solid'
}
},
axisLabel: {
color: '#909399',
fontSize: 12
}
},
series: data.value.series.map((item: any, index: number) => ({
name: item.name,
type: 'bar',
barGap: '10%',
data: item.data,
itemStyle: {
color: seriesColors[index % seriesColors.length],
borderRadius: [2, 2, 0, 0]
},
emphasis: {
itemStyle: {
opacity: 0.8
}
}
})),
legend: {
data: data.value.series.map((item: any) => item.name),
bottom: 0,
icon: 'rect',
itemWidth: 14,
itemHeight: 14,
itemGap: 20,
textStyle: {
color: '#606266',
fontSize: 12
}
}
}
chartInstance.setOption(option)
}
const onApiStatsDaily = (days: string) => {
ApiStatsDaily({ days }).then((res: { data: any[] }) => {
data.value = {
xAxis: res.data?.map((item) => formatISOToDateTime(item.date, 'YYYY-MM-DD')),
series: [
{
name: '联系人数',
data: res.data?.map((item) => item.contacts)
},
{
name: '已回复',
data: res.data?.map((item) => item.replied)
},
{
name: '微信交换',
data: res.data?.map((item) => item.wechat)
}
]
}
})
}
// 监听数据变化
watch(
() => data.value,
(newData: any) => {
if (chartInstance) {
chartInstance.setOption({
xAxis: { data: newData.xAxis },
series: newData.series.map((item: any, index: number) => ({
name: item.name,
data: item.data,
itemStyle: {
color: seriesColors[index % seriesColors.length]
}
})),
legend: {
data: newData.series.map((item: any) => item.name)
}
})
}
},
{ deep: true }
)
// 监听窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize()
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
window.removeEventListener('resize', handleResize)
})
defineExpose({
resize: handleResize,
onApiStatsDaily
})
</script>
<style scoped lang="scss">
.chart-container {
width: 100%;
padding: 20px;
background-color: #fff;
border-radius: 4px;
}
.trend-chart {
width: 100%;
height: v-bind(height);
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<div>TaskDetails</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="pre-registration-form">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="auto"
label-position="top"
>
<el-form-item label="任务类型" prop="task_type">
<el-select v-model="formData.task_type" placeholder="请选择">
<el-option label="检查登录" value="check_login" />
<el-option label="招聘" value="boss_recruit" />
<el-option label="回复" value="boss_reply" />
</el-select>
</el-form-item>
<el-form-item label="任务参数" prop="params">
<el-input v-model="formData.params" placeholder="请输入" />
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { deepCloneByJSON } from '@/utils/auxiliaryFunction'
const props = defineProps({
newData: {
type: Object,
default: () => ({})
}
})
const formRef = ref()
const formData = reactive<any>({
task_type: '',
worker_id: '',
account_name: '',
params: ''
})
const formRules = reactive<any>({
task_type: [{ required: true, message: '请输入', trigger: 'blur' }],
params: [{ required: true, message: '请输入', trigger: 'blur' }]
})
onMounted(() => {
setFormData(props.newData)
})
const getForm = () => {
return formData
}
const setFormData = (data: any) => {
if (data && Object.keys(data).length > 0) {
const data1 = deepCloneByJSON(data)
Object.assign(formData, data1)
}
}
const submit = (): Promise<boolean> => {
return new Promise((resolve, reject) => {
formRef.value?.validate((valid: boolean) => {
if (valid) {
resolve(true)
} else {
ElMessage.error('请完善必填信息')
reject(false)
}
})
})
}
defineExpose({
submit,
getForm
})
</script>
<style scoped lang="scss">
.pre-registration-form {
width: 100%;
padding-right: 20px;
overflow: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
}
.section {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div class="app-container">
<div class="headline-statistics">
<el-card shadow="hover" body-class="statistics-box" style="flex: 1">
<div class="statistics-box-img img-bg1">
<el-icon><User /></el-icon>
</div>
<div class="statistics-box-text">
<span>总联系人数</span>
<span>{{ apiStatsData.contacts?.total || 0 }}</span>
<span>今日{{ apiStatsData.contacts?.today || 0 }}</span>
</div>
</el-card>
<el-card shadow="hover" body-class="statistics-box" style="flex: 1">
<div class="statistics-box-img img-bg2">
<el-icon><ChatDotRound /></el-icon>
</div>
<div class="statistics-box-text">
<span>已回复人数</span>
<span>{{ apiStatsData.contacts?.replied || 0 }}</span>
<span>回复率{{ apiStatsData.contacts?.reply_rate || 0 }}%</span>
</div>
</el-card>
<el-card shadow="hover" body-class="statistics-box" style="flex: 1">
<div class="statistics-box-img img-bg3">
<el-icon><ChatLineRound /></el-icon>
</div>
<div class="statistics-box-text">
<span>微信交换数</span>
<span>{{ apiStatsData.wechat?.total || 0 }}</span>
<span>成功率{{ apiStatsData.wechat?.success_rate || 0 }}%</span>
</div>
</el-card>
<el-card shadow="hover" body-class="statistics-box" style="flex: 1">
<div class="statistics-box-img img-bg4">
<el-icon><Monitor /></el-icon>
</div>
<div class="statistics-box-text">
<span>在线Worker</span>
<span>{{ apiStatsData?.accounts?.logged_in || 0 }}</span>
<span>总数{{ apiStatsData?.accounts?.total || 0 }}</span>
</div>
</el-card>
</div>
<el-card shadow="hover">
<div class="chart-header">
<div class="chart-title">数据趋势</div>
<el-radio-group v-model="currentTime" size="small" @change="handleTimeChange">
<el-radio-button v-for="item in timeOptions" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<DataTrendChart ref="dataTrendChartRef" />
</el-card>
<el-card shadow="hover">
<DailyDataBreakdown></DailyDataBreakdown>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ApiStats } from '@/api/ContactInformation'
import { Monitor, User, ChatDotRound, ChatLineRound } from '@element-plus/icons-vue'
import DataTrendChart from './components/DataTrendChart.vue'
import { ref } from 'vue'
import DailyDataBreakdown from '@/views/DataStatistics/components/DailyDataBreakdown.vue'
defineOptions({
name: 'Role',
inheritAttrs: false
})
// const queryFormRef = ref()
const dataTrendChartRef = ref()
// 数据
const currentTime = ref('today')
const timeOptions = [
{ label: '今日', value: 'today' },
{ label: '近 7 天', value: 'week' },
{ label: '近 30 天', value: 'month' },
{ label: '全部', value: 'all' }
]
const apiStatsData = ref<any>({})
// // 弹窗
// const dialog = reactive({
// title: '',
// visible: false
// })
// 获取数据
const onApiStats = () => {
ApiStats({ period: currentTime.value }).then((res: any) => {
apiStatsData.value = res.data
})
}
// // 重置查询
// function handleResetQuery() {
// if (queryFormRef.value) queryFormRef.value?.resetFields()
// queryParams.pageNum = 1
// fetchData()
// }
const handleTimeChange = () => {
onApiStats()
}
onMounted(() => {
onApiStats()
dataTrendChartRef.value?.onApiStatsDaily('7')
})
</script>
<style lang="scss" scoped>
.headline-statistics {
display: flex;
gap: 20px;
margin-bottom: 16px;
height: 140px;
}
::v-deep(.statistics-box) {
display: flex;
//justify-content: center;
align-items: center;
color: #fff;
font-size: 30px;
gap: 20px;
.statistics-box-img {
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 15px;
}
.img-bg1 {
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
}
.img-bg2 {
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
}
.img-bg3 {
background: linear-gradient(135deg, #56bafd 0%, #16e9fd 100%);
}
.img-bg4 {
background: linear-gradient(135deg, #51eb90 0%, #49f7d2 100%);
}
.statistics-box-text {
//flex: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
color: #adb0b3;
& > span:nth-child(1) {
font-size: 14px;
}
& > span:nth-child(2) {
font-size: 26px;
color: #000;
font-weight: 700;
}
& > span:nth-child(3) {
font-size: 12px;
}
}
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-title {
font-size: 20px;
font-weight: 600;
color: #303133;
}
.time-filter {
display: flex;
gap: 8px;
:deep(.el-button) {
padding: 5px 12px;
font-size: 13px;
&.el-button--primary {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="pre-registration-form">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="auto"
label-position="top"
>
<el-form-item v-if="!newData?.id" label="配置名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入" />
<!-- <el-select v-model="formData.task_type" placeholder="请选择">-->
<!-- <el-option label="检查登录" value="check_login" />-->
<!-- <el-option label="招聘" value="boss_recruit" />-->
<!-- </el-select>-->
</el-form-item>
<el-form-item v-if="!newData?.id" label="岗位类型" prop="position">
<el-input v-model="formData.position" placeholder="请输入" />
</el-form-item>
<el-form-item label="复聊间隔天数" prop="followup_days">
<el-input v-model="formData.followup_days" placeholder="请输入" />
</el-form-item>
<el-form-item label="是否启用" prop="is_active">
<el-switch v-model="formData.is_active" />
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { deepCloneByJSON } from '@/utils/auxiliaryFunction'
const props = defineProps({
newData: {
type: Object,
default: () => ({})
}
})
const formRef = ref()
// 表单数据
const formData = reactive<any>({
name: '',
position: '',
followup_days: '',
is_active: false
})
const formRules = reactive<any>({
name: [{ required: true, message: '请输入', trigger: 'blur' }],
position: [{ required: true, message: '请输入', trigger: 'blur' }],
followup_days: [{ required: true, message: '请输入', trigger: 'blur' }],
is_active: [{ required: true, message: '请输入', trigger: 'blur' }]
})
onMounted(() => {
setFormData(props.newData)
})
const getForm = () => {
return formData
}
const setFormData = (data: any) => {
if (data && Object.keys(data).length > 0) {
const data1 = deepCloneByJSON(data)
Object.assign(formData, data1)
}
}
const submit = (): Promise<boolean> => {
return new Promise((resolve, reject) => {
formRef.value?.validate((valid: boolean) => {
if (valid) {
resolve(true)
} else {
ElMessage.error('请完善必填信息')
reject(false)
}
})
})
}
defineExpose({
submit,
getForm
})
</script>
<style scoped lang="scss">
.pre-registration-form {
width: 100%;
padding-right: 20px;
overflow: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
}
.section {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<div class="app-container">
<h3>复聊配置</h3>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="roleList"
highlight-current-row
row-key="id"
border
class="data-table__content"
:expanded-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
>
<el-table-column type="expand">
<template #default="scope">
<RevisedConversationScript
v-if="expandedRowKeys.includes(scope.row.id)"
:config-id="scope.row.id"
></RevisedConversationScript>
</template>
</el-table-column>
<el-table-column label="配置名称" prop="name" />
<el-table-column label="岗位类型" prop="position" />
<el-table-column label="复聊间隔" prop="followup_days" />
<el-table-column label="启用状态" prop="is_active">
<template #default="scope">
<el-switch v-model="scope.row.is_active" @change="onStatusModification(scope.row)" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_at">
<template #default="scope">
{{ formatISOToDateTime(scope.row?.created_at || '') }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
type="primary"
size="small"
link
icon="edit"
@click="handleExpandTask(scope.row.id)"
>
展开话术
</el-button>
<el-button
type="danger"
size="small"
link
icon="delete"
@click="onUserDeleteDepartment(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchData"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { RolePageVO } from '@/api/system/role-api'
import { functionDialogBox } from '@/utils/functionDialogBox'
import ReconfigurationSettingsForm from './components/ReconfigurationSettingsForm.vue'
import {
ApiFollowupConfigs,
ApiFollowupConfigsAdd,
ApiFollowupConfigsDelete,
ApiFollowupConfigsEditor
} from '@/api/ReconciliationManagement'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
import RevisedConversationScript from '../RevisedConversationScript/index.vue'
defineOptions({
name: 'Role',
inheritAttrs: false
})
const queryFormRef = ref()
const dataTableRef = ref()
const loading = ref(false)
const total = ref(0)
const expandedRowKeys = ref<string[]>([])
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
times: [],
seal_type: '',
CaseNumber: ''
})
// 表格数据
const roleList = ref<RolePageVO[]>()
// 弹窗
const dialog = reactive({
title: '',
visible: false
})
// 获取数据
function fetchData() {
loading.value = true
ApiFollowupConfigs()
.then((res: any) => {
roleList.value = res.data
// total.value = res.data.total
})
.finally(() => {
loading.value = false
})
}
// 查询(重置页码后获取数据)
function handleQuery() {
queryParams.pageNum = 1
fetchData()
}
// 重置查询
function handleResetQuery() {
if (queryFormRef.value) queryFormRef.value.resetFields()
queryParams.pageNum = 1
fetchData()
}
// 打开角色弹窗
function handleOpenDialog(data: any = null) {
dialog.visible = true
if (data) {
functionDialogBox(
ReconfigurationSettingsForm,
{
newData: data
},
{
title: '编辑复聊配置',
width: '900',
ok(value: any) {
handleSubmit({ id: data.id, ...value })
}
}
)
} else {
functionDialogBox(
ReconfigurationSettingsForm,
{},
{
title: '创建复聊配置',
width: '900',
ok(value: any) {
handleSubmit(value)
}
}
)
}
}
// 提交角色表单
function handleSubmit(data: any) {
loading.value = true
const roleId = data.id
if (roleId) {
ApiFollowupConfigsEditor(data)
.then(() => {
ElMessage.success('修改成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
} else {
ApiFollowupConfigsAdd(data)
.then(() => {
ElMessage.success('新增成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
}
}
const onUserDeleteDepartment = (id: string) => {
ElMessageBox.confirm('确认删除已选中的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(
() => {
loading.value = true
ApiFollowupConfigsDelete(id)
.then(() => {
ElMessage.success('删除成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
},
() => {
ElMessage.info('已取消删除')
}
)
}
function handleExpandChange(row: any, expandedRows: any[]) {
expandedRowKeys.value = expandedRows.map((item) => item.id)
}
const handleExpandTask = (rowId: string) => {
// 找到对应的行数据
const targetRow = roleList.value?.find((item) => item.id === rowId)
if (targetRow && dataTableRef.value) {
dataTableRef.value.toggleRowExpansion(targetRow)
}
}
const onStatusModification = (data: any) => {
ElMessageBox.confirm(
data.is_active ? '确认启用已选中的数据项?' : '确认禁用已选中的数据项?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
loading.value = true
ApiFollowupConfigsEditor({ is_active: data.is_active, id: data.id })
.then(() => {
ElMessage.success('修改成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
})
.catch(() => {
handleResetQuery()
})
}
onMounted(() => {
handleQuery()
})
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="pre-registration-form">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="auto"
label-position="top"
>
<el-form-item label="排序" prop="order">
<el-input v-model="formData.order" placeholder="请输入" />
</el-form-item>
<el-form-item label="话术内容" prop="content">
<el-input v-model="formData.content" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { deepCloneByJSON } from '@/utils/auxiliaryFunction'
const props = defineProps({
newData: {
type: Object,
default: () => ({})
}
})
const formRef = ref()
// 表单数据
const formData = reactive<any>({
order: '',
content: ''
})
const formRules = reactive<any>({
order: [{ required: true, message: '请输入', trigger: 'blur' }],
content: [{ required: true, message: '请输入', trigger: 'blur' }]
})
onMounted(() => {
setFormData(props.newData)
})
const getForm = () => {
return formData
}
const setFormData = (data: any) => {
if (data && Object.keys(data).length > 0) {
const data1 = deepCloneByJSON(data)
Object.assign(formData, data1)
}
}
const submit = (): Promise<boolean> => {
return new Promise((resolve, reject) => {
formRef.value?.validate((valid: boolean) => {
if (valid) {
resolve(true)
} else {
ElMessage.error('请完善必填信息')
reject(false)
}
})
})
}
defineExpose({
submit,
getForm
})
</script>
<style scoped lang="scss">
.pre-registration-form {
width: 100%;
padding-right: 20px;
overflow: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
}
.section {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<div class="app-container">
<h3>复聊话术</h3>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>
</div>
<div class="data-table__toolbar--actions">
<el-select
v-model="queryParams.day_number"
placeholder="请选择"
style="width: 100px"
@change="handleQuery()"
>
<el-option v-for="item in 5" :key="item" :label="`第${item}天`" :value="item">
{{ `${item}` }}
</el-option>
</el-select>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="roleList"
highlight-current-row
border
class="data-table__content"
@selection-change="handleSelectionChange"
>
<el-table-column label="第几天" prop="day_number">
<template #default="scope">{{ scope.row?.day_number }}</template>
</el-table-column>
<el-table-column label="排序" prop="order" />
<el-table-column label="话术内容" prop="content" />
<el-table-column label="创建时间" prop="created_at">
<template #default="scope">
{{ formatISOToDateTime(scope.row?.created_at || '') }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="140">
<template #default="scope">
<el-button
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
link
icon="delete"
@click="onUserDeleteDepartment(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchData"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { RolePageVO } from '@/api/system/role-api'
import { functionDialogBox } from '@/utils/functionDialogBox'
import RevisedConversationScriptForm from './components/RevisedConversationScriptForm.vue'
import {
ApiFollowupScripts,
ApiFollowupScriptsAdd,
ApiFollowupScriptsDelete,
ApiFollowupScriptsEditor
} from '@/api/ReconciliationManagement'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
const props = defineProps({
configId: {
type: String,
default: ''
}
})
defineOptions({
name: 'Role',
inheritAttrs: false
})
const queryFormRef = ref()
const loading = ref(false)
const ids = ref<number[]>([])
const total = ref(0)
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
day_number: 1
})
// 表格数据
const roleList = ref<RolePageVO[]>()
// 弹窗
const dialog = reactive({
title: '',
visible: false
})
// 获取数据
function fetchData() {
loading.value = true
ApiFollowupScripts({
config_id: props.configId,
day_number: queryParams.day_number
})
.then((res: any) => {
roleList.value = res.data
// total.value = res.data.total
})
.finally(() => {
loading.value = false
})
}
// 查询(重置页码后获取数据)
function handleQuery() {
queryParams.pageNum = 1
fetchData()
}
// 重置查询
function handleResetQuery() {
if (queryFormRef.value) queryFormRef.value.resetFields()
queryParams.pageNum = 1
fetchData()
}
// 行复选框选中
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id)
}
// 打开角色弹窗
function handleOpenDialog(data: any = null) {
dialog.visible = true
if (data) {
functionDialogBox(
RevisedConversationScriptForm,
{
newData: data
},
{
title: '编辑复聊话术',
width: '900',
ok(value: any) {
handleSubmit({ id: data.id, ...value })
}
}
)
} else {
functionDialogBox(
RevisedConversationScriptForm,
{},
{
title: '创建复聊话术',
width: '900',
ok(value: any) {
handleSubmit({ ...value, config_id: props.configId, day_number: queryParams.day_number })
}
}
)
}
}
// 提交角色表单
function handleSubmit(data: any) {
loading.value = true
const roleId = data.id
if (roleId) {
ApiFollowupScriptsEditor(data)
.then(() => {
ElMessage.success('修改成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
} else {
ApiFollowupScriptsAdd(data)
.then(() => {
ElMessage.success('新增成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
}
}
const onUserDeleteDepartment = (id: string) => {
ElMessageBox.confirm('确认删除已选中的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(
() => {
loading.value = true
ApiFollowupScriptsDelete(id)
.then(() => {
ElMessage.success('删除成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
},
() => {
ElMessage.info('已取消删除')
}
)
}
onMounted(() => {
handleQuery()
})
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="app-container">
<ReconfigurationSettings></ReconfigurationSettings>
<!-- <RevisedConversationScript></RevisedConversationScript>-->
</div>
</template>
<script setup lang="ts">
import ReconfigurationSettings from './components/ReconfigurationSettings/index.vue'
// import RevisedConversationScript from './components/RevisedConversationScript/index.vue'
</script>

View File

@@ -0,0 +1,137 @@
<template>
<div class="pre-registration-form">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="auto"
label-position="top"
>
<el-form-item label="配置名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入" />
<!-- <el-select v-model="formData.task_type" placeholder="请选择">-->
<!-- <el-option label="检查登录" value="check_login" />-->
<!-- <el-option label="招聘" value="boss_recruit" />-->
<!-- </el-select>-->
</el-form-item>
<el-form-item label="岗位关键词" prop="position_keywords">
<el-input v-model="formData.position_keywords" placeholder="请输入" />
</el-form-item>
<el-form-item label="城市" prop="city">
<el-input v-model="formData.city" placeholder="请输入" />
</el-form-item>
<el-form-item label="最低薪资k" prop="salary_min">
<el-input v-model="formData.salary_min" placeholder="请输入" />
</el-form-item>
<el-form-item label="最高薪资k" prop="salary_max">
<el-input v-model="formData.salary_max" placeholder="请输入" />
</el-form-item>
<el-form-item label="工作经验" prop="experience">
<el-input v-model="formData.experience" placeholder="请输入" />
</el-form-item>
<el-form-item label="学历要求" prop="education">
<el-input v-model="formData.education" placeholder="请输入" />
</el-form-item>
<el-form-item label="是否启用" prop="is_active">
<el-switch v-model="formData.is_active" />
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { deepCloneByJSON } from '@/utils/auxiliaryFunction'
const props = defineProps({
newData: {
type: Object,
default: () => ({})
}
})
const formRef = ref()
// 表单数据
const formData = reactive<any>({
name: '',
position_keywords: '',
city: '',
salary_min: '',
salary_max: '',
experience: '',
education: '',
is_active: false
})
const formRules = reactive<any>({
name: [{ required: true, message: '请输入', trigger: 'blur' }]
// position_keywords: [{ required: true, message: '请输入', trigger: 'blur' }],
// city: [{ required: true, message: '请输入', trigger: 'blur' }],
// salary_min: [{ required: true, message: '请输入', trigger: 'blur' }],
// experience: [{ required: true, message: '请输入', trigger: 'blur' }],
// education: [{ required: true, message: '请输入', trigger: 'blur' }],
// is_active: [{ required: true, message: '请输入', trigger: 'blur' }]
})
onMounted(() => {
setFormData(props.newData)
})
const getForm = () => {
return formData
}
const setFormData = (data: any) => {
if (data && Object.keys(data).length > 0) {
const data1 = deepCloneByJSON(data)
Object.assign(formData, data1)
}
}
const submit = (): Promise<boolean> => {
return new Promise((resolve, reject) => {
formRef.value?.validate((valid: boolean) => {
if (valid) {
resolve(true)
} else {
ElMessage.error('请完善必填信息')
reject(false)
}
})
})
}
defineExpose({
submit,
getForm
})
</script>
<style scoped lang="scss">
.pre-registration-form {
width: 100%;
padding-right: 20px;
overflow: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
}
.section {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>

View File

@@ -0,0 +1,240 @@
<template>
<div class="app-container">
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="roleList"
highlight-current-row
row-key="id"
border
class="data-table__content"
>
<el-table-column label="配置名称" prop="name" />
<el-table-column label="岗位关键词" prop="position_keywords" />
<el-table-column label="城市" prop="city" />
<el-table-column label="薪资范围单位k" prop="salary_min">
<template #default="scope">
{{ scope.row.salary_min || 0 }} - {{ scope.row.salary_max || 0 }}
</template>
</el-table-column>
<!-- <el-table-column label="复聊间隔" prop="salary_max" />-->
<el-table-column label="工作经验" prop="experience" />
<el-table-column label="学历要求" prop="education" />
<el-table-column label="启用状态" prop="is_active">
<template #default="scope">
<el-switch v-model="scope.row.is_active" @change="onStatusModification(scope.row)" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_at">
<template #default="scope">
{{ formatISOToDateTime(scope.row?.created_at || '') }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
link
icon="delete"
@click="onUserDeleteDepartment(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchData"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { RolePageVO } from '@/api/system/role-api'
import { functionDialogBox } from '@/utils/functionDialogBox'
import FilterSettingsForm from './components/FilterSettingsForm.vue'
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
import {
ApiFilters,
ApiFiltersAdd,
ApiFiltersDelete,
ApiFiltersEditor
} from '@/api/ScreeningManagement'
defineOptions({
name: 'Role',
inheritAttrs: false
})
const queryFormRef = ref()
const dataTableRef = ref()
const loading = ref(false)
const total = ref(0)
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
times: [],
seal_type: '',
CaseNumber: ''
})
// 表格数据
const roleList = ref<RolePageVO[]>()
// 弹窗
const dialog = reactive({
title: '',
visible: false
})
// 获取数据
function fetchData() {
loading.value = true
ApiFilters()
.then((res: any) => {
roleList.value = res.data
})
.finally(() => {
loading.value = false
})
}
// 查询(重置页码后获取数据)
function handleQuery() {
queryParams.pageNum = 1
fetchData()
}
// 重置查询
function handleResetQuery() {
if (queryFormRef.value) queryFormRef.value.resetFields()
queryParams.pageNum = 1
fetchData()
}
// 打开角色弹窗
function handleOpenDialog(data: any = null) {
dialog.visible = true
if (data) {
functionDialogBox(
FilterSettingsForm,
{
newData: data
},
{
title: '编辑筛选配置',
width: '900',
ok(value: any) {
handleSubmit({ id: data.id, ...value })
}
}
)
} else {
functionDialogBox(
FilterSettingsForm,
{},
{
title: '创建筛选配置',
width: '900',
ok(value: any) {
handleSubmit(value)
}
}
)
}
}
// 提交角色表单
function handleSubmit(data: any) {
loading.value = true
const roleId = data.id
if (roleId) {
ApiFiltersEditor(data)
.then(() => {
ElMessage.success('修改成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
} else {
ApiFiltersAdd(data)
.then(() => {
ElMessage.success('新增成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
}
}
const onUserDeleteDepartment = (id: string) => {
ElMessageBox.confirm('确认删除已选中的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(
() => {
loading.value = true
ApiFiltersDelete(id)
.then(() => {
ElMessage.success('删除成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
},
() => {
ElMessage.info('已取消删除')
}
)
}
const onStatusModification = (data: any) => {
ElMessageBox.confirm(
data.is_active ? '确认启用已选中的数据项?' : '确认禁用已选中的数据项?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
loading.value = true
ApiFiltersEditor({ is_active: data.is_active, id: data.id })
.then(() => {
ElMessage.success('修改成功')
handleResetQuery()
})
.finally(() => (loading.value = false))
})
.catch(() => {
console.log('取消修改')
handleResetQuery()
})
}
onMounted(() => {
handleQuery()
})
</script>

View File

@@ -8,10 +8,10 @@
label-position="top"
>
<el-form-item label="任务类型" prop="task_type">
<!-- <el-input v-model="formData.task_type" placeholder="请输入" />-->
<el-select v-model="formData.task_type" placeholder="请选择">
<el-option label="检查登录" value="check_login" />
<el-option label="招聘" value="boss_recruit" />
<el-option label="回复" value="boss_reply" />
</el-select>
</el-form-item>
<el-form-item label="任务参数" prop="params">
@@ -33,7 +33,6 @@ const props = defineProps({
})
const formRef = ref()
// 表单数据
const formData = reactive<any>({
task_type: '',
worker_id: '',
@@ -56,7 +55,6 @@ const getForm = () => {
const setFormData = (data: any) => {
if (data && Object.keys(data).length > 0) {
const data1 = deepCloneByJSON(data)
Object.assign(formData, data1)
}
}
@@ -73,6 +71,7 @@ const submit = (): Promise<boolean> => {
})
})
}
defineExpose({
submit,
getForm
@@ -108,3 +107,4 @@ defineExpose({
gap: 20px;
}
</style>