ha'ha
This commit is contained in:
@@ -9,8 +9,7 @@ VITE_APP_BASE_API=/dev-api
|
|||||||
VITE_APP_ENV = 'development'
|
VITE_APP_ENV = 'development'
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
# VITE_APP_API_URL=http://192.168.0.61:8007 # 本地
|
VITE_APP_API_URL=http://8.137.99.82:9000
|
||||||
VITE_APP_API_URL=http://26.151.107.60:8007 # 本地
|
|
||||||
|
|
||||||
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
|
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
|
||||||
VITE_APP_WS_ENDPOINT=
|
VITE_APP_WS_ENDPOINT=
|
||||||
|
|||||||
@@ -85,3 +85,12 @@ export const ApiRecruitFilterOptions = (accountId: string) => {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发筛选条件同步(派发任务给 Worker 抓取)
|
||||||
|
export const ApiRecruitFilterSync = (accountId: string) => {
|
||||||
|
return request({
|
||||||
|
url: `/api/filters/sync`,
|
||||||
|
method: 'post',
|
||||||
|
data: { account_id: accountId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
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'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import request from '@/utils/request'
|
|||||||
* 任务管理
|
* 任务管理
|
||||||
* */
|
* */
|
||||||
|
|
||||||
// 查询任务列表
|
// 查询指定账号的任务列表
|
||||||
export const ApiTasks = (boss_id: string, page: number, page_size: number) => {
|
export const ApiTasks = (boss_id: string, page: number, page_size: number) => {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/tasks/${boss_id}?page=${page}&page_size=${page_size}`,
|
url: `/api/tasks/${boss_id}?page=${page}&page_size=${page_size}`,
|
||||||
@@ -15,7 +15,7 @@ export const ApiTasks = (boss_id: string, page: number, page_size: number) => {
|
|||||||
// 查询指定任务的状态和结果
|
// 查询指定任务的状态和结果
|
||||||
export const ApiTasksTaskId = (task_id: string) => {
|
export const ApiTasksTaskId = (task_id: string) => {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/tasks/${task_id}`,
|
url: `/api/tasks/detail/${task_id}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,6 @@ export const ApiTasksTaskId = (task_id: string) => {
|
|||||||
export const ApiTasksAdd = (data: any) => {
|
export const ApiTasksAdd = (data: any) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('task_type', data.task_type)
|
formData.append('task_type', data.task_type)
|
||||||
// formData.append('worker_id', data.worker_id)
|
|
||||||
// formData.append('account_name', data.account_name)
|
|
||||||
formData.append('id', data.boss_id)
|
formData.append('id', data.boss_id)
|
||||||
const rawParams = data?.params
|
const rawParams = data?.params
|
||||||
if (typeof rawParams === 'string') {
|
if (typeof rawParams === 'string') {
|
||||||
@@ -43,31 +41,10 @@ export const ApiTasksAdd = (data: any) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 编辑申请用印
|
// 取消指定任务
|
||||||
// export const BusinessEditApplication = (data: any) => {
|
export const ApiTasksCancel = (task_id: string) => {
|
||||||
// const formData = new FormData()
|
return request({
|
||||||
// formData.append('id', data.id)
|
url: `/api/tasks/${task_id}/cancel`,
|
||||||
// formData.append('Printingpurpose', data.Printingpurpose)
|
method: 'post'
|
||||||
// formData.append('case_id', data.case_id)
|
})
|
||||||
// formData.append('Reason', data.Reason)
|
}
|
||||||
// formData.append('seal_number', data.seal_number)
|
|
||||||
// formData.append('seal_type', data.seal_type)
|
|
||||||
// if (isFile(data.file)) formData.append('file', data.file)
|
|
||||||
// formData.append('approvers', data.approvers)
|
|
||||||
// return request({
|
|
||||||
// url: `/business/editApplication`,
|
|
||||||
// method: 'post',
|
|
||||||
// data: formData,
|
|
||||||
// headers: {
|
|
||||||
// 'Content-Type': 'multipart/form-data'
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // 删除指定账号
|
|
||||||
// export const ApiAccountsDelete = (id: string) => {
|
|
||||||
// return request({
|
|
||||||
// url: `/api/accounts/${id}`,
|
|
||||||
// method: 'delete'
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 命令请求参数
|
|
||||||
*/
|
|
||||||
export interface AiCommandRequest {
|
|
||||||
/** 用户输入的自然语言命令 */
|
|
||||||
command: string
|
|
||||||
/** 当前页面路由(用于上下文) */
|
|
||||||
currentRoute?: string
|
|
||||||
/** 当前激活的组件名称 */
|
|
||||||
currentComponent?: string
|
|
||||||
/** 额外上下文信息 */
|
|
||||||
context?: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 函数调用参数
|
|
||||||
*/
|
|
||||||
export interface FunctionCall {
|
|
||||||
/** 函数名称 */
|
|
||||||
name: string
|
|
||||||
/** 函数描述 */
|
|
||||||
description?: string
|
|
||||||
/** 参数对象 */
|
|
||||||
arguments: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 命令解析响应
|
|
||||||
*/
|
|
||||||
export interface AiCommandResponse {
|
|
||||||
/** 解析日志ID(用于关联执行记录) */
|
|
||||||
parseLogId?: string
|
|
||||||
/** 是否成功解析 */
|
|
||||||
success: boolean
|
|
||||||
/** 解析后的函数调用列表 */
|
|
||||||
functionCalls: FunctionCall[]
|
|
||||||
/** AI 的理解和说明 */
|
|
||||||
explanation?: string
|
|
||||||
/** 置信度 (0-1) */
|
|
||||||
confidence?: number
|
|
||||||
/** 错误信息 */
|
|
||||||
error?: string
|
|
||||||
/** 原始 LLM 响应(用于调试) */
|
|
||||||
rawResponse?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 命令执行请求
|
|
||||||
*/
|
|
||||||
export interface AiExecuteRequest {
|
|
||||||
/** 关联的解析日志ID */
|
|
||||||
parseLogId?: string
|
|
||||||
/** 原始命令(用于审计) */
|
|
||||||
originalCommand?: string
|
|
||||||
/** 要执行的函数调用 */
|
|
||||||
functionCall: FunctionCall
|
|
||||||
/** 确认模式:auto=自动执行, manual=需要用户确认 */
|
|
||||||
confirmMode?: 'auto' | 'manual'
|
|
||||||
/** 用户确认标志 */
|
|
||||||
userConfirmed?: boolean
|
|
||||||
/** 幂等性令牌(防止重复执行) */
|
|
||||||
idempotencyKey?: string
|
|
||||||
/** 当前页面路由 */
|
|
||||||
currentRoute?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 命令执行响应
|
|
||||||
*/
|
|
||||||
export interface AiExecuteResponse {
|
|
||||||
/** 是否执行成功 */
|
|
||||||
success: boolean
|
|
||||||
/** 执行结果数据 */
|
|
||||||
data?: any
|
|
||||||
/** 执行结果说明 */
|
|
||||||
message?: string
|
|
||||||
/** 影响的记录数 */
|
|
||||||
affectedRows?: number
|
|
||||||
/** 错误信息 */
|
|
||||||
error?: string
|
|
||||||
/** 记录ID(用于追踪) */
|
|
||||||
recordId?: string
|
|
||||||
/** 需要用户确认 */
|
|
||||||
requiresConfirmation?: boolean
|
|
||||||
/** 确认提示信息 */
|
|
||||||
confirmationPrompt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AiCommandRecordPageQuery extends PageQuery {
|
|
||||||
keywords?: string
|
|
||||||
executeStatus?: string
|
|
||||||
parseSuccess?: boolean
|
|
||||||
userId?: number
|
|
||||||
isDangerous?: boolean
|
|
||||||
provider?: string
|
|
||||||
model?: string
|
|
||||||
functionName?: string
|
|
||||||
createTime?: [string, string]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AiCommandRecordVO {
|
|
||||||
id: string
|
|
||||||
userId: number
|
|
||||||
username: string
|
|
||||||
originalCommand: string
|
|
||||||
provider?: string
|
|
||||||
model?: string
|
|
||||||
parseSuccess?: boolean
|
|
||||||
functionCalls?: string
|
|
||||||
explanation?: string
|
|
||||||
confidence?: number
|
|
||||||
parseErrorMessage?: string
|
|
||||||
inputTokens?: number
|
|
||||||
outputTokens?: number
|
|
||||||
totalTokens?: number
|
|
||||||
parseTime?: number
|
|
||||||
functionName?: string
|
|
||||||
functionArguments?: string
|
|
||||||
executeStatus?: string
|
|
||||||
executeResult?: string
|
|
||||||
executeErrorMessage?: string
|
|
||||||
affectedRows?: number
|
|
||||||
isDangerous?: boolean
|
|
||||||
requiresConfirmation?: boolean
|
|
||||||
userConfirmed?: boolean
|
|
||||||
executionTime?: number
|
|
||||||
ipAddress?: string
|
|
||||||
userAgent?: string
|
|
||||||
currentRoute?: string
|
|
||||||
createTime?: string
|
|
||||||
updateTime?: string
|
|
||||||
remark?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 命令 API
|
|
||||||
*/
|
|
||||||
class AiCommandApi {
|
|
||||||
/**
|
|
||||||
* 解析自然语言命令
|
|
||||||
*
|
|
||||||
* @param data 命令请求参数
|
|
||||||
* @returns 解析结果
|
|
||||||
*/
|
|
||||||
static parseCommand(data: AiCommandRequest): Promise<AiCommandResponse> {
|
|
||||||
return request<any, AiCommandResponse>({
|
|
||||||
url: '/api/v1/ai/command/parse',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行已解析的命令
|
|
||||||
*
|
|
||||||
* @param data 执行请求参数
|
|
||||||
* @returns 执行结果数据(成功时返回,失败时抛出异常)
|
|
||||||
*/
|
|
||||||
static executeCommand(data: AiExecuteRequest): Promise<any> {
|
|
||||||
return request<any, any>({
|
|
||||||
url: '/api/v1/ai/command/execute',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取命令记录分页列表
|
|
||||||
*/
|
|
||||||
static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) {
|
|
||||||
return request<any, PageResult<AiCommandRecordVO[]>>({
|
|
||||||
url: '/api/v1/ai/command/records',
|
|
||||||
method: 'get',
|
|
||||||
params: queryParams
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 撤销命令执行(如果支持)
|
|
||||||
*/
|
|
||||||
static rollbackCommand(recordId: string) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/ai/command/rollback/${recordId}`,
|
|
||||||
method: 'post'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AiCommandApi
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
const AUTH_BASE_URL = '/api/v1/auth'
|
|
||||||
|
|
||||||
const AuthAPI = {
|
|
||||||
/** 登录接口*/
|
|
||||||
login(data: LoginFormData) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('username', 'admin')
|
|
||||||
formData.append('password', data.password)
|
|
||||||
formData.append('captchaKey', data.captchaKey)
|
|
||||||
formData.append('captchaCode', data.captchaCode)
|
|
||||||
return request<any, LoginResult>({
|
|
||||||
url: `${AUTH_BASE_URL}/login`,
|
|
||||||
method: 'post',
|
|
||||||
data: formData,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 刷新 token 接口*/
|
|
||||||
refreshToken(refreshToken: string) {
|
|
||||||
return request<any, LoginResult>({
|
|
||||||
url: `${AUTH_BASE_URL}/refresh-token`,
|
|
||||||
method: 'post',
|
|
||||||
params: { refreshToken },
|
|
||||||
headers: {
|
|
||||||
Authorization: 'no-auth'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 退出登录接口 */
|
|
||||||
logout() {
|
|
||||||
return request({
|
|
||||||
url: `${AUTH_BASE_URL}/logout`,
|
|
||||||
method: 'delete'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 获取验证码接口*/
|
|
||||||
getCaptcha() {
|
|
||||||
return request<any, CaptchaInfo>({
|
|
||||||
url: `${AUTH_BASE_URL}/captcha`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AuthAPI
|
|
||||||
|
|
||||||
/** 登录表单数据 */
|
|
||||||
export interface LoginFormData {
|
|
||||||
/** 用户名 */
|
|
||||||
username: string
|
|
||||||
/** 密码 */
|
|
||||||
password: string
|
|
||||||
/** 验证码缓存key */
|
|
||||||
captchaKey: string
|
|
||||||
/** 验证码 */
|
|
||||||
captchaCode: string
|
|
||||||
/** 记住我 */
|
|
||||||
rememberMe: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 登录响应 */
|
|
||||||
export interface LoginResult {
|
|
||||||
/** 访问令牌 */
|
|
||||||
accessToken: string
|
|
||||||
/** 刷新令牌 */
|
|
||||||
refreshToken: string
|
|
||||||
/** 令牌类型 */
|
|
||||||
tokenType: string
|
|
||||||
/** 过期时间(秒) */
|
|
||||||
expiresIn: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 验证码信息 */
|
|
||||||
export interface CaptchaInfo {
|
|
||||||
/** 验证码缓存key */
|
|
||||||
captchaKey: string
|
|
||||||
/** 验证码图片Base64字符串 */
|
|
||||||
captchaBase64: string
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import type { InternalAxiosRequestConfig } from 'axios'
|
|
||||||
import { useUserStoreHook } from '@/store/modules/user-store'
|
|
||||||
import { AuthStorage, redirectToLogin } from '@/utils/auth'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试请求的回调函数类型
|
|
||||||
*/
|
|
||||||
type RetryCallback = () => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token刷新组合式函数
|
|
||||||
*/
|
|
||||||
export function useTokenRefresh() {
|
|
||||||
// Token 刷新相关状态
|
|
||||||
let isRefreshingToken = false
|
|
||||||
const pendingRequests: RetryCallback[] = []
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新 Token 并重试请求
|
|
||||||
*/
|
|
||||||
async function refreshTokenAndRetry(
|
|
||||||
config: InternalAxiosRequestConfig,
|
|
||||||
httpRequest: any
|
|
||||||
): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// 封装需要重试的请求
|
|
||||||
const retryRequest = () => {
|
|
||||||
const newToken = AuthStorage.getAccessToken()
|
|
||||||
if (newToken && config.headers) {
|
|
||||||
config.headers.Authorization = `Bearer ${newToken}`
|
|
||||||
}
|
|
||||||
httpRequest(config).then(resolve).catch(reject)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将请求加入等待队列
|
|
||||||
pendingRequests.push(retryRequest)
|
|
||||||
|
|
||||||
// 如果没有正在刷新,则开始刷新流程
|
|
||||||
if (!isRefreshingToken) {
|
|
||||||
isRefreshingToken = true
|
|
||||||
|
|
||||||
useUserStoreHook()
|
|
||||||
.refreshToken()
|
|
||||||
.then(() => {
|
|
||||||
// 刷新成功,重试所有等待的请求
|
|
||||||
pendingRequests.forEach((callback) => {
|
|
||||||
try {
|
|
||||||
callback()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Retry request error:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 清空队列
|
|
||||||
pendingRequests.length = 0
|
|
||||||
})
|
|
||||||
.catch(async (error) => {
|
|
||||||
console.error('Token refresh failed:', error)
|
|
||||||
// 刷新失败,先 reject 所有等待的请求,再清空队列
|
|
||||||
const failedRequests = [...pendingRequests]
|
|
||||||
pendingRequests.length = 0
|
|
||||||
|
|
||||||
// 拒绝所有等待的请求
|
|
||||||
failedRequests.forEach(() => {
|
|
||||||
reject(new Error('Token refresh failed'))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 跳转登录页
|
|
||||||
await redirectToLogin('登录状态已失效,请重新登录')
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isRefreshingToken = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取刷新状态(用于外部判断)
|
|
||||||
*/
|
|
||||||
function getRefreshStatus() {
|
|
||||||
return {
|
|
||||||
isRefreshing: isRefreshingToken,
|
|
||||||
pendingCount: pendingRequests.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
refreshTokenAndRetry,
|
|
||||||
getRefreshStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ export { useStomp } from './websocket/useStomp'
|
|||||||
export { useDictSync } from './websocket/useDictSync'
|
export { useDictSync } from './websocket/useDictSync'
|
||||||
export type { DictMessage } from './websocket/useDictSync'
|
export type { DictMessage } from './websocket/useDictSync'
|
||||||
export { useOnlineCount } from './websocket/useOnlineCount'
|
export { useOnlineCount } from './websocket/useOnlineCount'
|
||||||
export { useTokenRefresh } from './auth/useTokenRefresh'
|
|
||||||
|
|
||||||
export { useLayout } from './layout/useLayout'
|
export { useLayout } from './layout/useLayout'
|
||||||
export { useLayoutMenu } from './layout/useLayoutMenu'
|
export { useLayoutMenu } from './layout/useLayoutMenu'
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { onMounted, onBeforeUnmount, nextTick } from 'vue'
|
import { onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import AiCommandApi from '@/api/ai'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 操作处理器(简化版)
|
* AI 操作处理器(简化版)
|
||||||
@@ -11,17 +10,17 @@ import AiCommandApi from '@/api/ai'
|
|||||||
export type AiActionHandler<T = any> =
|
export type AiActionHandler<T = any> =
|
||||||
| ((args: T) => Promise<void> | void)
|
| ((args: T) => Promise<void> | void)
|
||||||
| {
|
| {
|
||||||
/** 执行函数 */
|
/** 执行函数 */
|
||||||
execute: (args: T) => Promise<void> | void
|
execute: (args: T) => Promise<void> | void
|
||||||
/** 是否需要确认(默认 true) */
|
/** 是否需要确认(默认 true) */
|
||||||
needConfirm?: boolean
|
needConfirm?: boolean
|
||||||
/** 确认消息(支持函数或字符串) */
|
/** 确认消息(支持函数或字符串) */
|
||||||
confirmMessage?: string | ((args: T) => string)
|
confirmMessage?: string | ((args: T) => string)
|
||||||
/** 成功消息(支持函数或字符串) */
|
/** 成功消息(支持函数或字符串) */
|
||||||
successMessage?: string | ((args: T) => string)
|
successMessage?: string | ((args: T) => string)
|
||||||
/** 是否调用后端 API(默认 false,如果为 true 则自动调用 executeCommand) */
|
/** 是否调用后端 API(默认 false,如果为 true 则自动调用 executeCommand) */
|
||||||
callBackendApi?: boolean
|
callBackendApi?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 操作配置
|
* AI 操作配置
|
||||||
@@ -104,17 +103,8 @@ export function useAiAction(options: UseAiActionOptions = {}) {
|
|||||||
|
|
||||||
// 2. 执行阶段
|
// 2. 执行阶段
|
||||||
if (config.callBackendApi) {
|
if (config.callBackendApi) {
|
||||||
// 自动调用后端 API
|
// AI 后端 API 暂未实现
|
||||||
await AiCommandApi.executeCommand({
|
ElMessage.warning('AI 后端接口暂未实现')
|
||||||
originalCommand: action.originalCommand || '',
|
|
||||||
confirmMode: 'manual',
|
|
||||||
userConfirmed: true,
|
|
||||||
currentRoute,
|
|
||||||
functionCall: {
|
|
||||||
name: fnCall.name,
|
|
||||||
arguments: fnCall.arguments
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
// 执行自定义函数
|
// 执行自定义函数
|
||||||
await config.execute(fnCall.arguments)
|
await config.execute(fnCall.arguments)
|
||||||
@@ -179,24 +169,8 @@ export function useAiAction(options: UseAiActionOptions = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// AI 后端 API 暂未实现
|
||||||
await AiCommandApi.executeCommand({
|
ElMessage.warning('AI 后端接口暂未实现')
|
||||||
originalCommand,
|
|
||||||
confirmMode,
|
|
||||||
userConfirmed: true,
|
|
||||||
currentRoute,
|
|
||||||
functionCall: {
|
|
||||||
name: functionName,
|
|
||||||
arguments: args
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ElMessage.success('操作执行成功')
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error !== 'cancel') {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,21 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
export const enum ApiCodeEnum {
|
export const enum ApiCodeEnum {
|
||||||
/**
|
/**
|
||||||
* 成功
|
* 成功(后端 api_success 返回 code=0)
|
||||||
*/
|
*/
|
||||||
SUCCESS = '00000',
|
SUCCESS = 0,
|
||||||
/**
|
/**
|
||||||
* 错误
|
* 未认证 / Token 无效(后端返回 HTTP 401)
|
||||||
*/
|
*/
|
||||||
ERROR = 'B0001',
|
UNAUTHORIZED = 401,
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问令牌无效或过期
|
|
||||||
*/
|
|
||||||
ACCESS_TOKEN_INVALID = 'A0230',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新令牌无效或过期
|
|
||||||
*/
|
|
||||||
REFRESH_TOKEN_INVALID = 'A0231'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,25 +362,6 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/screening',
|
|
||||||
name: 'Screening',
|
|
||||||
component: Layout,
|
|
||||||
meta: {
|
|
||||||
title: '筛选管理',
|
|
||||||
icon: 'setting'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'screeningManagement',
|
|
||||||
name: 'ScreeningManagement',
|
|
||||||
component: () => import('@/views/ScreeningManagement/index.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '筛选管理'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
// 注册平台登记
|
// 注册平台登记
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const authConfig = {
|
|||||||
*
|
*
|
||||||
* 适用场景:后端没有刷新接口或不需要自动刷新的项目可设为false
|
* 适用场景:后端没有刷新接口或不需要自动刷新的项目可设为false
|
||||||
*/
|
*/
|
||||||
enableTokenRefresh: true
|
enableTokenRefresh: false
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 主题色预设 - 现代配色方案(基于示例模板风格)
|
// 主题色预设 - 现代配色方案(基于示例模板风格)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { store } from '@/store'
|
import { store } from '@/store'
|
||||||
|
|
||||||
import AuthAPI from '@/api/auth-api'
|
|
||||||
import type { UserInfo } from '@/api/system/user-api'
|
import type { UserInfo } from '@/api/system/user-api'
|
||||||
|
|
||||||
import { AuthStorage } from '@/utils/auth'
|
import { AuthStorage } from '@/utils/auth'
|
||||||
@@ -107,31 +106,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
setPermissionCharactersArr([])
|
setPermissionCharactersArr([])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新 token
|
|
||||||
*/
|
|
||||||
function refreshToken() {
|
|
||||||
const refreshToken = AuthStorage.getRefreshToken()
|
|
||||||
|
|
||||||
if (!refreshToken) {
|
|
||||||
return Promise.reject(new Error('没有有效的刷新令牌'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
AuthAPI.refreshToken(refreshToken)
|
|
||||||
.then((data) => {
|
|
||||||
const { accessToken, refreshToken: newRefreshToken } = data
|
|
||||||
// 更新令牌,保持当前记住我状态
|
|
||||||
AuthStorage.setTokens(accessToken, newRefreshToken, AuthStorage.getRememberMe())
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(' refreshToken 刷新失败', error)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkPermission = (permission: string): boolean => {
|
const checkPermission = (permission: string): boolean => {
|
||||||
if (userInfo.value?.permission_data.includes('*:*:*')) {
|
if (userInfo.value?.permission_data.includes('*:*:*')) {
|
||||||
return true
|
return true
|
||||||
@@ -149,7 +123,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
logout,
|
logout,
|
||||||
resetAllState,
|
resetAllState,
|
||||||
resetUserState,
|
resetUserState,
|
||||||
refreshToken,
|
|
||||||
permissionCharactersArr,
|
permissionCharactersArr,
|
||||||
setPermissionCharactersArr,
|
setPermissionCharactersArr,
|
||||||
checkPermission
|
checkPermission
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@use "./reset";
|
@use "./reset";
|
||||||
@use "./element-plus";
|
@use "./element-plus";
|
||||||
@use "./template-style"; // 示例模板风格
|
@use "./template-style"; // 示例模板风格
|
||||||
|
@use "./ops-ui";
|
||||||
// Vxe Table
|
// Vxe Table
|
||||||
@use "./vxe-table";
|
@use "./vxe-table";
|
||||||
@import url("./vxe-table.css");
|
@import url("./vxe-table.css");
|
||||||
@@ -190,4 +191,4 @@ html.sidebar-color-blue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/styles/ops-ui.scss
Normal file
79
src/styles/ops-ui.scss
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
--ops-radius-lg: 14px;
|
||||||
|
--ops-radius-md: 10px;
|
||||||
|
--ops-border-color: #e8edf5;
|
||||||
|
--ops-soft-bg: #f7f9fc;
|
||||||
|
--ops-hover-bg: #f5f9ff;
|
||||||
|
--ops-title-color: #1f2d3d;
|
||||||
|
--ops-text-regular: #303133;
|
||||||
|
--ops-text-secondary: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-page {
|
||||||
|
.ops-card {
|
||||||
|
border: 1px solid var(--ops-border-color);
|
||||||
|
border-radius: var(--ops-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-section-title {
|
||||||
|
color: var(--ops-title-color);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-table {
|
||||||
|
.el-table__header th {
|
||||||
|
background: var(--ops-soft-bg);
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row:hover > td {
|
||||||
|
background: var(--ops-hover-bg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-status-tag {
|
||||||
|
min-width: 64px;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-btn {
|
||||||
|
--el-button-border-radius: 10px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-btn.el-button--small {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ops-page {
|
||||||
|
.ops-btn {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/types/global.d.ts
vendored
6
src/types/global.d.ts
vendored
@@ -3,7 +3,7 @@ declare global {
|
|||||||
* 响应数据
|
* 响应数据
|
||||||
*/
|
*/
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
code: string
|
code: number
|
||||||
data: T
|
data: T
|
||||||
msg: string
|
msg: string
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
interface ExcelResult {
|
interface ExcelResult {
|
||||||
/** 状态码 */
|
/** 状态码 */
|
||||||
code: string
|
code: number
|
||||||
/** 无效数据条数 */
|
/** 无效数据条数 */
|
||||||
invalidCount: number
|
invalidCount: number
|
||||||
/** 有效数据条数 */
|
/** 有效数据条数 */
|
||||||
@@ -108,4 +108,4 @@ declare global {
|
|||||||
messageList: Array<string>
|
messageList: Array<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export {}
|
export { }
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axio
|
|||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
import { ApiCodeEnum } from '@/enums/api/code-enum'
|
import { ApiCodeEnum } from '@/enums/api/code-enum'
|
||||||
import { AuthStorage, redirectToLogin } from '@/utils/auth'
|
import { AuthStorage, redirectToLogin } from '@/utils/auth'
|
||||||
import { useTokenRefresh } from '@/composables/auth/useTokenRefresh'
|
|
||||||
import { authConfig } from '@/settings'
|
|
||||||
|
|
||||||
// 初始化token刷新组合式函数
|
|
||||||
const { refreshTokenAndRetry } = useTokenRefresh()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 HTTP 请求实例
|
* 创建 HTTP 请求实例
|
||||||
@@ -50,8 +45,8 @@ httpRequest.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { code, msg } = response.data
|
const { code, msg } = response.data
|
||||||
// 请求成功
|
// 请求成功:后端成功响应 code === 0
|
||||||
if (code === ApiCodeEnum.SUCCESS || code == '0' || code == '200') {
|
if (code === ApiCodeEnum.SUCCESS) {
|
||||||
return response.data as any
|
return response.data as any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +57,7 @@ httpRequest.interceptors.response.use(
|
|||||||
async (error) => {
|
async (error) => {
|
||||||
console.error('Response interceptor error:', error)
|
console.error('Response interceptor error:', error)
|
||||||
|
|
||||||
const { config, response } = error
|
const { response } = error
|
||||||
|
|
||||||
// 网络错误或服务器无响应
|
// 网络错误或服务器无响应
|
||||||
if (!response) {
|
if (!response) {
|
||||||
@@ -70,29 +65,16 @@ httpRequest.interceptors.response.use(
|
|||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { code, message } = response.data as any
|
// HTTP 401:Token 无效或过期,直接跳转登录页
|
||||||
|
if (response.status === 401) {
|
||||||
switch (code) {
|
await redirectToLogin('登录已过期,请重新登录')
|
||||||
case ApiCodeEnum.ACCESS_TOKEN_INVALID:
|
return Promise.reject(new Error('Token Invalid'))
|
||||||
// Access Token 过期
|
|
||||||
if (authConfig.enableTokenRefresh) {
|
|
||||||
// 启用了token刷新,尝试刷新
|
|
||||||
return refreshTokenAndRetry(config, httpRequest)
|
|
||||||
} else {
|
|
||||||
// 未启用token刷新,直接跳转登录页
|
|
||||||
await redirectToLogin('登录已过期,请重新登录')
|
|
||||||
return Promise.reject(new Error(message || 'Access Token Invalid'))
|
|
||||||
}
|
|
||||||
|
|
||||||
case ApiCodeEnum.REFRESH_TOKEN_INVALID:
|
|
||||||
// Refresh Token 过期,跳转登录页
|
|
||||||
await redirectToLogin('登录已过期,请重新登录')
|
|
||||||
return Promise.reject(new Error(message || 'Refresh Token Invalid'))
|
|
||||||
|
|
||||||
default:
|
|
||||||
ElMessage.error(message || '请求失败')
|
|
||||||
return Promise.reject(new Error(message || 'Request Error'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 其他错误
|
||||||
|
const msg = response.data?.msg || response.data?.message || '请求失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
return Promise.reject(new Error(msg))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="recruit-task-form">
|
<div class="recruit-task-form">
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="syncedAt"
|
v-if="syncing"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
title="正在从网页同步筛选条件,请稍候..."
|
||||||
|
/>
|
||||||
|
<el-alert
|
||||||
|
v-else-if="syncedAt"
|
||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
@@ -17,26 +25,59 @@
|
|||||||
title="当前账号暂无筛选项,请先启动客户端同步筛选项"
|
title="当前账号暂无筛选项,请先启动客户端同步筛选项"
|
||||||
/>
|
/>
|
||||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="auto" label-position="top">
|
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="auto" label-position="top">
|
||||||
|
<!-- 分组筛选条件(排除"年龄"分组) -->
|
||||||
<el-form-item label="筛选条件(可多选)" prop="selected_filters">
|
<el-form-item label="筛选条件(可多选)" prop="selected_filters">
|
||||||
<el-select
|
<div v-if="optionsLoading" class="filter-loading">
|
||||||
v-model="formData.selected_filters"
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
multiple
|
<span>加载中...</span>
|
||||||
filterable
|
</div>
|
||||||
clearable
|
<div v-else-if="displayGroups.length === 0" class="filter-empty">
|
||||||
collapse-tags
|
暂无可选筛选项
|
||||||
collapse-tags-tooltip
|
</div>
|
||||||
:loading="optionsLoading"
|
<div v-else class="filter-groups">
|
||||||
placeholder="请选择筛选条件"
|
<div v-for="group in displayGroups" :key="group.name" class="filter-group">
|
||||||
style="width: 100%"
|
<div class="filter-group__title">{{ group.name }}</div>
|
||||||
>
|
<el-checkbox-group v-model="formData.selected_filters" class="filter-group__options">
|
||||||
<el-option
|
<el-checkbox
|
||||||
v-for="item in filterOptions"
|
v-for="opt in group.options"
|
||||||
:key="item"
|
:key="group.name + ':' + opt"
|
||||||
:label="item"
|
:label="opt"
|
||||||
:value="item"
|
:value="group.name + ':' + opt"
|
||||||
/>
|
border
|
||||||
</el-select>
|
size="small"
|
||||||
|
/>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 年龄区间(独立控制,不走浏览器筛选) -->
|
||||||
|
<el-form-item label="年龄范围(从候选人信息过滤,不在网页点击筛选)">
|
||||||
|
<div class="age-range">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.age_min"
|
||||||
|
:min="16"
|
||||||
|
:max="65"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="最小"
|
||||||
|
style="width: 140px"
|
||||||
|
/>
|
||||||
|
<span class="age-range__sep">至</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.age_max"
|
||||||
|
:min="16"
|
||||||
|
:max="65"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="最大"
|
||||||
|
style="width: 140px"
|
||||||
|
/>
|
||||||
|
<span class="age-range__unit">岁</span>
|
||||||
|
<el-button link type="primary" @click="clearAgeRange" style="margin-left: 8px">清除</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="招聘人数" prop="greet_target">
|
<el-form-item label="招聘人数" prop="greet_target">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.greet_target"
|
v-model="formData.greet_target"
|
||||||
@@ -46,13 +87,48 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="启用问候语数量" prop="greeting_count">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.greeting_count"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="问候语内容(多条时随机发送)" prop="greeting_messages">
|
||||||
|
<div class="greeting-list">
|
||||||
|
<div v-for="(_, index) in formData.greeting_messages" :key="`greeting-${index}`" class="greeting-item">
|
||||||
|
<div class="greeting-item__label">问候语 {{ index + 1 }}</div>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.greeting_messages[index]"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="请输入问候语"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="greeting-tip">
|
||||||
|
系统内置问候语:{{ DEFAULT_GREETING_MESSAGE }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { ApiRecruitFilterOptions } from '@/api/BoosAccountManagement'
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
import { ApiRecruitFilterOptions, ApiRecruitFilterSync } from '@/api/BoosAccountManagement'
|
||||||
|
|
||||||
|
interface FilterGroup {
|
||||||
|
name: string
|
||||||
|
order: number
|
||||||
|
options: string[]
|
||||||
|
}
|
||||||
|
const DEFAULT_GREETING_MESSAGE = '当前岗位还在招,加个微信了解一下吗?'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
accountId: {
|
accountId: {
|
||||||
@@ -63,52 +139,154 @@ const props = defineProps({
|
|||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const optionsLoading = ref(false)
|
const optionsLoading = ref(false)
|
||||||
const filterOptions = ref<string[]>([])
|
const filterGroups = ref<FilterGroup[]>([])
|
||||||
const syncedAt = ref('')
|
const syncedAt = ref('')
|
||||||
|
const syncing = ref(false)
|
||||||
|
|
||||||
|
// 排除"年龄"相关的分组,展示给用户的分组
|
||||||
|
const displayGroups = computed(() => {
|
||||||
|
return filterGroups.value.filter(
|
||||||
|
(g) => !g.name.includes('年龄')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
selected_filters: [] as string[],
|
selected_filters: [] as string[],
|
||||||
greet_target: 20
|
greet_target: 20,
|
||||||
|
age_min: null as number | null,
|
||||||
|
age_max: null as number | null,
|
||||||
|
greeting_count: 1,
|
||||||
|
greeting_messages: [DEFAULT_GREETING_MESSAGE] as string[]
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = reactive<any>({
|
const formRules = reactive<any>({
|
||||||
selected_filters: [{ required: true, message: '请至少选择一个筛选条件', trigger: 'change' }],
|
greet_target: [{ required: true, message: '请输入招聘人数', trigger: 'blur' }],
|
||||||
greet_target: [{ required: true, message: '请输入招聘人数', trigger: 'blur' }]
|
greeting_count: [{ required: true, message: '请输入问候语数量', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const clearAgeRange = () => {
|
||||||
|
formData.age_min = null
|
||||||
|
formData.age_max = null
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
const fetchOptions = () => {
|
const fetchOptions = () => {
|
||||||
if (!props.accountId) return
|
if (!props.accountId) return
|
||||||
optionsLoading.value = true
|
optionsLoading.value = true
|
||||||
ApiRecruitFilterOptions(props.accountId)
|
ApiRecruitFilterOptions(props.accountId)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
filterOptions.value = (res.data?.flat_options || []) as string[]
|
filterGroups.value = (res.data?.groups || []) as FilterGroup[]
|
||||||
syncedAt.value = res.data?.synced_at || ''
|
syncedAt.value = res.data?.synced_at || ''
|
||||||
|
// 没有筛选数据时自动触发同步
|
||||||
|
if (filterGroups.value.length === 0 && !syncing.value) {
|
||||||
|
triggerSync()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
optionsLoading.value = false
|
optionsLoading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const triggerSync = () => {
|
||||||
|
if (!props.accountId || syncing.value) return
|
||||||
|
syncing.value = true
|
||||||
|
ApiRecruitFilterSync(props.accountId)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.info('正在同步筛选条件,请稍候...')
|
||||||
|
startPolling()
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
syncing.value = false
|
||||||
|
const msg = err?.response?.data?.msg || err?.msg || '同步请求失败'
|
||||||
|
ElMessage.warning(msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPolling = () => {
|
||||||
|
stopPolling()
|
||||||
|
let attempts = 0
|
||||||
|
const maxAttempts = 20
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
attempts++
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
stopPolling()
|
||||||
|
syncing.value = false
|
||||||
|
ElMessage.warning('同步超时,请稍后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ApiRecruitFilterOptions(props.accountId)
|
||||||
|
.then((res: any) => {
|
||||||
|
const groups = (res.data?.groups || []) as FilterGroup[]
|
||||||
|
if (groups.length > 0) {
|
||||||
|
filterGroups.value = groups
|
||||||
|
syncedAt.value = res.data?.synced_at || ''
|
||||||
|
stopPolling()
|
||||||
|
syncing.value = false
|
||||||
|
ElMessage.success('筛选条件同步完成')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
normalizeGreetingSlots()
|
||||||
fetchOptions()
|
fetchOptions()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
|
||||||
const getForm = () => {
|
const getForm = () => {
|
||||||
|
// 内部 value 格式为 "groupName:optName",返回给后端时只取选项名部分
|
||||||
|
// 并过滤掉"不限",勾选不限等于不加筛选条件,无需传给后端
|
||||||
|
const filters = formData.selected_filters
|
||||||
|
.map((v) => {
|
||||||
|
const idx = v.indexOf(':')
|
||||||
|
return idx !== -1 ? v.slice(idx + 1) : v
|
||||||
|
})
|
||||||
|
.filter((opt) => opt !== '不限')
|
||||||
|
const greetings = formData.greeting_messages
|
||||||
|
.slice(0, Number(formData.greeting_count) || 1)
|
||||||
|
.map((text) => String(text || '').trim())
|
||||||
|
.filter((text) => !!text)
|
||||||
|
const greetingMessages = Array.from(new Set(greetings.length ? greetings : [DEFAULT_GREETING_MESSAGE]))
|
||||||
return {
|
return {
|
||||||
selected_filters: [...formData.selected_filters],
|
selected_filters: filters,
|
||||||
greet_target: Number(formData.greet_target) || 20
|
greet_target: Number(formData.greet_target) || 20,
|
||||||
|
age_min: formData.age_min ?? undefined,
|
||||||
|
age_max: formData.age_max ?? undefined,
|
||||||
|
greeting_count: Number(formData.greeting_count) || 1,
|
||||||
|
greeting_messages: greetingMessages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = (): Promise<boolean> => {
|
const submit = (): Promise<boolean> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (filterOptions.value.length === 0) {
|
|
||||||
ElMessage.error('暂无可选筛选项,请先启动客户端同步筛选项')
|
|
||||||
reject(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
formRef.value?.validate((valid: boolean) => {
|
formRef.value?.validate((valid: boolean) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
// 年龄范围校验
|
||||||
|
if (formData.age_min != null && formData.age_max != null && formData.age_min > formData.age_max) {
|
||||||
|
ElMessage.error('最小年龄不能大于最大年龄')
|
||||||
|
reject(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hasGreeting = formData.greeting_messages
|
||||||
|
.slice(0, Number(formData.greeting_count) || 1)
|
||||||
|
.some((item) => String(item || '').trim().length > 0)
|
||||||
|
if (!hasGreeting) {
|
||||||
|
ElMessage.error('请至少填写 1 条问候语')
|
||||||
|
reject(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
resolve(true)
|
resolve(true)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('请完善招聘参数')
|
ElMessage.error('请完善招聘参数')
|
||||||
@@ -118,6 +296,23 @@ const submit = (): Promise<boolean> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeGreetingSlots = () => {
|
||||||
|
const normalizedCount = Math.max(1, Math.min(10, Number(formData.greeting_count) || 1))
|
||||||
|
formData.greeting_count = normalizedCount
|
||||||
|
if (formData.greeting_messages.length > normalizedCount) {
|
||||||
|
formData.greeting_messages = formData.greeting_messages.slice(0, normalizedCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
while (formData.greeting_messages.length < normalizedCount) {
|
||||||
|
formData.greeting_messages.push(DEFAULT_GREETING_MESSAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formData.greeting_count,
|
||||||
|
() => normalizeGreetingSlots()
|
||||||
|
)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
submit,
|
submit,
|
||||||
getForm
|
getForm
|
||||||
@@ -129,5 +324,90 @@ defineExpose({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
.filter-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-empty {
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-groups {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
&__title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 2px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-range {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__sep {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__unit {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting-item {
|
||||||
|
&__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,56 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container account-page ops-page">
|
||||||
<!-- 搜索区域 -->
|
<div class="page-header">
|
||||||
<!-- <div class="search-container">-->
|
<div>
|
||||||
<!-- <el-form ref="queryFormRef" :model="queryParams" :inline="true">-->
|
<div class="page-header__title">BOSS 账号管理</div>
|
||||||
<!-- <el-form-item prop="seal_type" label="用印用途">-->
|
<div class="page-header__desc">统一管理账号登录状态、Worker 在线状态和任务执行进度</div>
|
||||||
<!-- <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>-->
|
|
||||||
<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>
|
</div>
|
||||||
|
<div class="page-header__actions ops-actions">
|
||||||
|
<el-button type="primary" icon="plus" class="ops-btn" @click="handleOpenDialog()">
|
||||||
|
新增账号环境
|
||||||
|
</el-button>
|
||||||
|
<el-button icon="refresh" class="ops-btn" @click="fetchData()">刷新数据</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overview-grid">
|
||||||
|
<el-card
|
||||||
|
v-for="card in overviewCards"
|
||||||
|
:key="card.label"
|
||||||
|
shadow="hover"
|
||||||
|
class="overview-card ops-card"
|
||||||
|
:class="card.tone"
|
||||||
|
>
|
||||||
|
<div class="overview-card__icon">
|
||||||
|
<el-icon><component :is="card.icon" /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="overview-card__content">
|
||||||
|
<p>{{ card.label }}</p>
|
||||||
|
<h3>{{ card.value }}</h3>
|
||||||
|
<span>{{ card.sub }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="table-panel ops-card">
|
||||||
|
<div class="table-panel__toolbar ops-toolbar">
|
||||||
|
<div class="table-tip">系统每 15 秒自动刷新账号与 Worker 状态</div>
|
||||||
|
<el-tag type="info" effect="light">已展开任务:{{ expandedRowKeys.length }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
ref="dataTableRef"
|
ref="dataTableRef"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="roleList"
|
:data="roleList"
|
||||||
|
stripe
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
row-key="id"
|
row-key="id"
|
||||||
border
|
border
|
||||||
|
empty-text="暂无账号数据"
|
||||||
:expanded-row-keys="expandedRowKeys"
|
:expanded-row-keys="expandedRowKeys"
|
||||||
class="data-table__content"
|
class="account-table ops-table"
|
||||||
@expand-change="handleExpandChange"
|
@expand-change="handleExpandChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="expand">
|
<el-table-column type="expand">
|
||||||
@@ -59,163 +57,102 @@
|
|||||||
v-if="expandedRowKeys.includes(scope.row.id)"
|
v-if="expandedRowKeys.includes(scope.row.id)"
|
||||||
ref="taskListRef"
|
ref="taskListRef"
|
||||||
:boss-id="scope.row.id"
|
:boss-id="scope.row.id"
|
||||||
></ViewTheTaskListDetails>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="环境名称" prop="browser_name" />
|
<el-table-column label="环境名称" prop="browser_name" min-width="150" />
|
||||||
<el-table-column label="电脑标识" prop="worker_id" />
|
<el-table-column label="账号ID" prop="id" width="110" />
|
||||||
<el-table-column label="浏览器环境名称" prop="browser_name" />
|
<el-table-column label="电脑标识" prop="worker_id" min-width="150" />
|
||||||
<el-table-column label="登录昵称" prop="boss_username" />
|
<el-table-column label="登录昵称" prop="boss_username" min-width="120" />
|
||||||
<el-table-column label="电脑名称" prop="worker_name">
|
<el-table-column label="电脑名称" prop="worker_name" min-width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span v-if="scope.row.worker_name">
|
<span>{{ scope.row.worker_name || '--' }}</span>
|
||||||
{{ scope.row.worker_name }}
|
|
||||||
</span>
|
|
||||||
<span v-else>--</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column label="是否登录 BOSS" prop="is_logged_in" width="130" align="center">
|
||||||
label="否已登录 BOSS"
|
|
||||||
prop="is_logged_in"
|
|
||||||
width="140"
|
|
||||||
align="center"
|
|
||||||
header-align="center"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.is_logged_in" type="success">
|
<el-tag
|
||||||
{{ '在线' }}
|
:type="scope.row.is_logged_in ? 'success' : 'danger'"
|
||||||
</el-tag>
|
effect="light"
|
||||||
<el-tag v-else type="danger">
|
class="status-tag ops-status-tag"
|
||||||
{{ '离线' }}
|
>
|
||||||
|
{{ scope.row.is_logged_in ? '在线' : '离线' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column label="Worker 状态" prop="worker_online" width="120" align="center">
|
||||||
label="Worker 是否在线"
|
|
||||||
prop="worker_online"
|
|
||||||
width="140"
|
|
||||||
align="center"
|
|
||||||
header-align="center"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.worker_online" type="success">
|
<el-tag
|
||||||
{{ '在线' }}
|
:type="scope.row.worker_online ? 'success' : 'danger'"
|
||||||
</el-tag>
|
effect="light"
|
||||||
<el-tag v-else type="danger">
|
class="status-tag ops-status-tag"
|
||||||
{{ '离线' }}
|
>
|
||||||
|
{{ scope.row.worker_online ? '在线' : '离线' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column label="任务状态" prop="current_task_status" width="110" align="center">
|
||||||
label="任务状态"
|
|
||||||
prop="current_task_status"
|
|
||||||
width="100"
|
|
||||||
align="center"
|
|
||||||
header-align="center"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.current_task_status === 'pending'" type="primary">
|
<el-tag
|
||||||
{{ '待派发' }}
|
v-if="scope.row.current_task_status"
|
||||||
</el-tag>
|
:type="getTaskStatusMeta(scope.row.current_task_status).type"
|
||||||
<el-tag v-else-if="scope.row.current_task_status === 'dispatched'" type="warning">
|
effect="light"
|
||||||
{{ '已派发' }}
|
class="status-tag ops-status-tag"
|
||||||
</el-tag>
|
>
|
||||||
<el-tag v-else-if="scope.row.current_task_status === 'running'" type="warning">
|
{{ getTaskStatusMeta(scope.row.current_task_status).label }}
|
||||||
{{ '执行中' }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag v-else-if="scope.row.current_task_status === 'success'" type="success">
|
|
||||||
{{ '成功' }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag v-else-if="scope.row.current_task_status === 'failed'" type="danger">
|
|
||||||
{{ '失败' }}
|
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-else>--</span>
|
<span v-else>--</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="更新时间" prop="updated_at">
|
<el-table-column label="更新时间" prop="updated_at" min-width="170">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatISOToDateTime(scope.row?.updated_at || '') }}
|
{{ scope.row?.updated_at ? formatISOToDateTime(scope.row.updated_at) : '--' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column fixed="right" label="操作" width="300">
|
<el-table-column fixed="right" label="操作" width="360">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<div class="action-group ops-actions">
|
||||||
type="warning"
|
<el-button
|
||||||
size="small"
|
type="warning"
|
||||||
@click="onApiTasksAdd(scope.row.id, 'boss_reply', {})"
|
size="small"
|
||||||
>
|
class="ops-btn"
|
||||||
回复
|
@click="onApiTasksAdd(scope.row.id, 'boss_reply', {})"
|
||||||
</el-button>
|
>
|
||||||
<!-- <el-button-->
|
回复
|
||||||
<!-- type="primary"-->
|
</el-button>
|
||||||
<!-- size="small"-->
|
<el-button
|
||||||
<!-- link-->
|
type="primary"
|
||||||
<!-- icon="edit"-->
|
size="small"
|
||||||
<!-- @click="onDetails(scope.row.id)"-->
|
class="ops-btn"
|
||||||
<!-- >-->
|
@click="onApiTasksAdd(scope.row.id, 'check_login', {})"
|
||||||
<!-- 详情-->
|
>
|
||||||
<!-- </el-button>-->
|
检查登录
|
||||||
<!-- <el-button-->
|
</el-button>
|
||||||
<!-- type="primary"-->
|
<el-button type="success" size="small" class="ops-btn" @click="openRecruitDialog(scope.row.id)">
|
||||||
<!-- size="small"-->
|
招聘
|
||||||
<!-- link-->
|
</el-button>
|
||||||
<!-- icon="edit"-->
|
<el-button
|
||||||
<!-- @click="onApiTasksAdd(scope.row.id)"-->
|
type="primary"
|
||||||
<!-- >-->
|
size="small"
|
||||||
<!-- 发布任务-->
|
link
|
||||||
<!-- </el-button>-->
|
icon="edit"
|
||||||
<el-button
|
@click="handleExpandTask(scope.row.id)"
|
||||||
type="primary"
|
>
|
||||||
size="small"
|
{{ expandedRowKeys.includes(scope.row.id) ? '收起任务' : '展开任务' }}
|
||||||
@click="onApiTasksAdd(scope.row.id, 'check_login', {})"
|
</el-button>
|
||||||
>
|
<el-button
|
||||||
检查登录
|
type="danger"
|
||||||
</el-button>
|
size="small"
|
||||||
<el-button
|
link
|
||||||
type="success"
|
icon="delete"
|
||||||
size="small"
|
@click="onUserDeleteDepartment(scope.row.id)"
|
||||||
@click="openRecruitDialog(scope.row.id)"
|
>
|
||||||
>
|
删除
|
||||||
招聘
|
</el-button>
|
||||||
</el-button>
|
</div>
|
||||||
<!-- <el-button-->
|
|
||||||
<!-- type="primary"-->
|
|
||||||
<!-- size="small"-->
|
|
||||||
<!-- link-->
|
|
||||||
<!-- icon="edit"-->
|
|
||||||
<!-- @click="onDetails(scope.row.id)"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 查看任务-->
|
|
||||||
<!-- </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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -229,9 +166,9 @@ import { BusinessEditApplication } from '@/api/calibration/applicationForSealApp
|
|||||||
import { ApiAccounts, ApiAccountsAdd, ApiAccountsDelete } from '@/api/BoosAccountManagement'
|
import { ApiAccounts, ApiAccountsAdd, ApiAccountsDelete } from '@/api/BoosAccountManagement'
|
||||||
import ViewTheTaskListDetails from './components/ViewTheTaskListDetails.vue'
|
import ViewTheTaskListDetails from './components/ViewTheTaskListDetails.vue'
|
||||||
import { ApiTasksAdd } from '@/api/TaskManagement'
|
import { ApiTasksAdd } from '@/api/TaskManagement'
|
||||||
// import TaskForm from '@/views/TaskManagement/components/TaskForm.vue'
|
|
||||||
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
|
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
|
||||||
import { createTimer } from '@/utils/TimerManager'
|
import { createTimer } from '@/utils/TimerManager'
|
||||||
|
import { User, Monitor, ChatDotRound, Message } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Role',
|
name: 'Role',
|
||||||
@@ -239,60 +176,89 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { startTimer, stopTimer } = createTimer()
|
const { startTimer, stopTimer } = createTimer()
|
||||||
|
|
||||||
const queryFormRef = ref()
|
|
||||||
const dataTableRef = ref()
|
const dataTableRef = ref()
|
||||||
const taskListRef = ref()
|
const taskListRef = ref()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const total = ref(0)
|
const roleList = ref<RolePageVO[]>([])
|
||||||
|
const expandedRowKeys = ref<Array<string | number>>([])
|
||||||
|
|
||||||
const queryParams = reactive<any>({
|
const TASK_STATUS_MAP: Record<
|
||||||
pageNum: 1,
|
string,
|
||||||
pageSize: 10,
|
{
|
||||||
times: [],
|
label: string
|
||||||
seal_type: '',
|
type: 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
||||||
CaseNumber: ''
|
}
|
||||||
|
> = {
|
||||||
|
pending: { label: '待派发', type: 'info' },
|
||||||
|
dispatched: { label: '已派发', type: 'warning' },
|
||||||
|
running: { label: '执行中', type: 'primary' },
|
||||||
|
success: { label: '成功', type: 'success' },
|
||||||
|
failed: { label: '失败', type: 'danger' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTaskStatusMeta = (status: string) =>
|
||||||
|
TASK_STATUS_MAP[status] || {
|
||||||
|
label: '--',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountStats = computed(() => {
|
||||||
|
const list = roleList.value || []
|
||||||
|
return {
|
||||||
|
total: list.length,
|
||||||
|
loggedIn: list.filter((item: any) => Boolean(item.is_logged_in)).length,
|
||||||
|
workerOnline: list.filter((item: any) => Boolean(item.worker_online)).length,
|
||||||
|
runningTasks: list.filter((item: any) => item.current_task_status === 'running').length
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表格数据
|
const overviewCards = computed(() => [
|
||||||
const roleList = ref<RolePageVO[]>()
|
{
|
||||||
const expandedRowKeys = ref<string[]>([])
|
label: '账号总数',
|
||||||
// 弹窗
|
value: accountStats.value.total,
|
||||||
const dialog = reactive({
|
sub: `已登录:${accountStats.value.loggedIn}`,
|
||||||
title: '',
|
icon: User,
|
||||||
visible: false
|
tone: 'tone-1'
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
label: '在线 Worker',
|
||||||
|
value: accountStats.value.workerOnline,
|
||||||
|
sub: `总 Worker:${accountStats.value.total}`,
|
||||||
|
icon: Monitor,
|
||||||
|
tone: 'tone-2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '运行中任务',
|
||||||
|
value: accountStats.value.runningTasks,
|
||||||
|
sub: '任务状态实时同步',
|
||||||
|
icon: ChatDotRound,
|
||||||
|
tone: 'tone-3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '待处理账号',
|
||||||
|
value: Math.max(accountStats.value.total - accountStats.value.loggedIn, 0),
|
||||||
|
sub: '建议优先检查登录',
|
||||||
|
icon: Message,
|
||||||
|
tone: 'tone-4'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// 获取数据
|
function fetchData(silent = false) {
|
||||||
function fetchData() {
|
if (!silent) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
}
|
||||||
ApiAccounts()
|
ApiAccounts()
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
roleList.value = res.data
|
roleList.value = res.data || []
|
||||||
// total.value = res.total
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false
|
if (!silent) {
|
||||||
|
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) {
|
function handleOpenDialog(data: any = null) {
|
||||||
dialog.visible = true
|
|
||||||
if (data) {
|
if (data) {
|
||||||
functionDialogBox(
|
functionDialogBox(
|
||||||
BoosAccountForm,
|
BoosAccountForm,
|
||||||
@@ -322,7 +288,6 @@ function handleOpenDialog(data: any = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交角色表单
|
|
||||||
function handleSubmit(data: any) {
|
function handleSubmit(data: any) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const roleId = data.id
|
const roleId = data.id
|
||||||
@@ -330,14 +295,14 @@ function handleSubmit(data: any) {
|
|||||||
BusinessEditApplication(data)
|
BusinessEditApplication(data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ElMessage.success('修改成功')
|
ElMessage.success('修改成功')
|
||||||
handleResetQuery()
|
fetchData()
|
||||||
})
|
})
|
||||||
.finally(() => (loading.value = false))
|
.finally(() => (loading.value = false))
|
||||||
} else {
|
} else {
|
||||||
ApiAccountsAdd(data)
|
ApiAccountsAdd(data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ElMessage.success('操作成功')
|
ElMessage.success('操作成功')
|
||||||
handleResetQuery()
|
fetchData()
|
||||||
})
|
})
|
||||||
.finally(() => (loading.value = false))
|
.finally(() => (loading.value = false))
|
||||||
}
|
}
|
||||||
@@ -354,7 +319,7 @@ const onUserDeleteDepartment = (id: string) => {
|
|||||||
ApiAccountsDelete(id)
|
ApiAccountsDelete(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
handleResetQuery()
|
fetchData()
|
||||||
})
|
})
|
||||||
.finally(() => (loading.value = false))
|
.finally(() => (loading.value = false))
|
||||||
},
|
},
|
||||||
@@ -364,20 +329,6 @@ const onUserDeleteDepartment = (id: string) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// const onDetails = (id: string) => {
|
|
||||||
// functionDialogBox(
|
|
||||||
// ViewTheTaskListDetails,
|
|
||||||
// {
|
|
||||||
// bossId: id
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: '任务列表',
|
|
||||||
// width: '1200',
|
|
||||||
// footerVisible: false
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
const openRecruitDialog = (id: string) => {
|
const openRecruitDialog = (id: string) => {
|
||||||
functionDialogBox(
|
functionDialogBox(
|
||||||
RecruitTaskForm,
|
RecruitTaskForm,
|
||||||
@@ -395,55 +346,204 @@ const openRecruitDialog = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onApiTasksAdd = (id: string, task_type: string, params: any = {}) => {
|
const onApiTasksAdd = (id: string, task_type: string, params: any = {}) => {
|
||||||
// functionDialogBox(
|
|
||||||
// TaskForm,
|
|
||||||
// {},
|
|
||||||
// {
|
|
||||||
// title: '提交新任务',
|
|
||||||
// width: '900',
|
|
||||||
// ok(value: any) {
|
|
||||||
// ApiTasksAdd({ ...value, boss_id: id })
|
|
||||||
// .then(() => {
|
|
||||||
// ElMessage.success('提交成功')
|
|
||||||
// handleResetQuery()
|
|
||||||
// })
|
|
||||||
// .finally(() => (loading.value = false))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
ApiTasksAdd({ params, task_type, boss_id: id })
|
ApiTasksAdd({ params, task_type, boss_id: id })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
ElMessage.success(res.msg)
|
ElMessage.success(res.msg)
|
||||||
handleResetQuery()
|
fetchData()
|
||||||
refreshTaskList()
|
refreshTaskList()
|
||||||
})
|
})
|
||||||
.finally(() => (loading.value = false))
|
.finally(() => (loading.value = false))
|
||||||
}
|
}
|
||||||
function handleExpandChange(row: any, expandedRows: any[]) {
|
|
||||||
|
function handleExpandChange(_row: any, expandedRows: any[]) {
|
||||||
expandedRowKeys.value = expandedRows.map((item) => item.id)
|
expandedRowKeys.value = expandedRows.map((item) => item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExpandTask = (rowId: string) => {
|
const handleExpandTask = (rowId: string | number) => {
|
||||||
// 找到对应的行数据
|
const targetRow = roleList.value?.find((item: any) => item.id === rowId)
|
||||||
const targetRow = roleList.value?.find((item) => item.id === rowId)
|
|
||||||
if (targetRow && dataTableRef.value) {
|
if (targetRow && dataTableRef.value) {
|
||||||
dataTableRef.value.toggleRowExpansion(targetRow)
|
dataTableRef.value.toggleRowExpansion(targetRow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshTaskList = () => {
|
const refreshTaskList = () => {
|
||||||
// 调用子组件的 fetchData 方法
|
|
||||||
taskListRef.value?.fetchData()
|
taskListRef.value?.fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// handleQuery()
|
fetchData()
|
||||||
startTimer(() => {
|
startTimer(() => {
|
||||||
handleQuery()
|
fetchData(true)
|
||||||
})
|
}, 15000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopTimer()
|
stopTimer()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.account-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 6px 0 4px;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-1 .overview-card__icon {
|
||||||
|
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-2 .overview-card__icon {
|
||||||
|
background: linear-gradient(135deg, #56bafd 0%, #16e9fd 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-3 .overview-card__icon {
|
||||||
|
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-4 .overview-card__icon {
|
||||||
|
background: linear-gradient(135deg, #36d1dc 0%, #5b86e5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-panel {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-table {
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f6f8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:hover > td) {
|
||||||
|
background: #f5f9ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
min-width: 62px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header__title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header__actions,
|
||||||
|
.table-panel__toolbar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container contacts-page ops-page">
|
||||||
<!-- 搜索区域 -->
|
<div class="page-header">
|
||||||
<div class="search-container">
|
<div class="page-header__title">联系人信息</div>
|
||||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
<div class="page-header__desc">统一查看候选人联系状态、回复进度和微信交换情况</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="search-panel ops-card">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="search-form">
|
||||||
<el-form-item prop="search" label="姓名">
|
<el-form-item prop="search" label="姓名">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.search"
|
v-model="queryParams.search"
|
||||||
placeholder="请输入"
|
placeholder="请输入候选人姓名"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleQuery"
|
@keyup.enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
@@ -16,92 +20,74 @@
|
|||||||
v-model="queryParams.times"
|
v-model="queryParams.times"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="请选择创建时间"
|
|
||||||
range-separator="至"
|
range-separator="至"
|
||||||
start-placeholder="开始时间"
|
start-placeholder="开始时间"
|
||||||
end-placeholder="结束时间"
|
end-placeholder="结束时间"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item class="search-buttons">
|
<el-form-item class="search-actions ops-actions">
|
||||||
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" :icon="Search" class="ops-btn" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
<el-button :icon="Refresh" class="ops-btn" @click="handleResetQuery">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</el-card>
|
||||||
<el-card shadow="hover" class="data-table">
|
|
||||||
<div class="data-table__toolbar">
|
<el-card shadow="hover" class="table-panel ops-card">
|
||||||
<div class="data-table__toolbar--actions">
|
<div class="table-panel__toolbar ops-toolbar">
|
||||||
<!-- <el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>-->
|
<div class="toolbar-summary">
|
||||||
</div>
|
<span class="summary-item">当前页 {{ pageCount }} 人</span>
|
||||||
<div class="data-table__toolbar--actions">
|
<span class="summary-item summary-item--success">已回复 {{ repliedCount }} 人</span>
|
||||||
<el-button type="success" icon="Download" @click="onApiContactsExport()">导出</el-button>
|
<span class="summary-item summary-item--wechat">已交换微信 {{ wechatCount }} 人</span>
|
||||||
</div>
|
</div>
|
||||||
|
<el-button type="success" :icon="Download" class="ops-btn" @click="onApiContactsExport">导出联系人</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
ref="dataTableRef"
|
ref="dataTableRef"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="roleList"
|
:data="roleList"
|
||||||
|
stripe
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
border
|
border
|
||||||
class="data-table__content"
|
empty-text="暂无联系人数据"
|
||||||
@selection-change="handleSelectionChange"
|
class="contacts-table ops-table"
|
||||||
>
|
>
|
||||||
<el-table-column label="候选人姓名" prop="name" />
|
<el-table-column label="候选人姓名" prop="name" min-width="120" />
|
||||||
<el-table-column label="应聘岗位" prop="position" />
|
<el-table-column label="应聘岗位" prop="position" min-width="130" />
|
||||||
<el-table-column label="联系方式(电话号或微信号)" prop="contact" />
|
<el-table-column label="联系方式(电话号或微信号)" prop="contact" min-width="180" />
|
||||||
<el-table-column label="回复状态" prop="reply_status">
|
<el-table-column label="回复状态" prop="reply_status" width="110" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.reply_status === '已回复'" type="success">
|
<el-tag
|
||||||
{{ scope.row.reply_status }}
|
:type="scope.row.reply_status === '已回复' ? 'success' : 'danger'"
|
||||||
</el-tag>
|
effect="light"
|
||||||
<el-tag v-else type="danger">
|
class="status-tag ops-status-tag"
|
||||||
{{ scope.row.reply_status }}
|
>
|
||||||
|
{{ scope.row.reply_status || '未回复' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="微信交换" prop="wechat_exchanged">
|
<el-table-column label="微信交换" prop="wechat_exchanged" width="110" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.wechat_exchanged" type="success">
|
<el-tag
|
||||||
{{ '已交换' }}
|
:type="scope.row.wechat_exchanged ? 'success' : 'info'"
|
||||||
</el-tag>
|
effect="light"
|
||||||
<el-tag v-else type="danger">
|
class="status-tag ops-status-tag"
|
||||||
{{ '未交换' }}
|
>
|
||||||
|
{{ scope.row.wechat_exchanged ? '已交换' : '未交换' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="备注信息" prop="notes" />
|
<el-table-column label="备注信息" prop="notes" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column label="联系时间" prop="contacted_at">
|
<el-table-column label="联系时间" prop="contacted_at" min-width="170">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatISOToDateTime(scope.row?.contacted_at || '') }}
|
{{ scope.row?.contacted_at ? formatISOToDateTime(scope.row.contacted_at) : '--' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" prop="created_at">
|
<el-table-column label="创建时间" prop="created_at" min-width="170">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatISOToDateTime(scope.row?.created_at || '') }}
|
{{ scope.row?.created_at ? formatISOToDateTime(scope.row.created_at) : '--' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- <el-table-column fixed="right" label="操作" width="140">-->
|
|
||||||
<!-- <template #default="scope">-->
|
|
||||||
<!-- <el-button-->
|
|
||||||
<!-- type="primary"-->
|
|
||||||
<!-- size="small"-->
|
|
||||||
<!-- link-->
|
|
||||||
<!-- icon="edit"-->
|
|
||||||
<!-- @click="onDetails(scope.row.task_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>
|
</el-table>
|
||||||
|
|
||||||
<pagination
|
<pagination
|
||||||
@@ -118,6 +104,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ApiContacts, ApiContactsExport } from '@/api/ContactInformation'
|
import { ApiContacts, ApiContactsExport } from '@/api/ContactInformation'
|
||||||
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
|
import { formatISOToDateTime } from '@/utils/auxiliaryFunction'
|
||||||
|
import { Download, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Role',
|
name: 'Role',
|
||||||
@@ -125,9 +112,7 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const queryFormRef = ref()
|
const queryFormRef = ref()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const ids = ref<number[]>([])
|
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|
||||||
const queryParams = reactive<any>({
|
const queryParams = reactive<any>({
|
||||||
@@ -137,46 +122,40 @@ const queryParams = reactive<any>({
|
|||||||
search: ''
|
search: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表格数据
|
const roleList = ref<any[]>([])
|
||||||
const roleList = ref<any[]>()
|
const pageCount = computed(() => roleList.value.length)
|
||||||
|
const repliedCount = computed(() =>
|
||||||
|
roleList.value.filter((item: any) => item.reply_status === '已回复').length
|
||||||
|
)
|
||||||
|
const wechatCount = computed(() =>
|
||||||
|
roleList.value.filter((item: any) => Boolean(item.wechat_exchanged)).length
|
||||||
|
)
|
||||||
|
|
||||||
// // 弹窗
|
|
||||||
// const dialog = reactive({
|
|
||||||
// title: '',
|
|
||||||
// visible: false
|
|
||||||
// })
|
|
||||||
|
|
||||||
// 获取数据
|
|
||||||
function fetchData() {
|
function fetchData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
ApiContacts(queryParams)
|
ApiContacts(queryParams)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
roleList.value = res.data.results
|
roleList.value = res.data.results || []
|
||||||
total.value = res.data.total
|
total.value = res.data.total || 0
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询(重置页码后获取数据)
|
|
||||||
function handleQuery() {
|
function handleQuery() {
|
||||||
queryParams.pageNum = 1
|
queryParams.pageNum = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 重置查询
|
|
||||||
function handleResetQuery() {
|
function handleResetQuery() {
|
||||||
if (queryFormRef.value) queryFormRef.value?.resetFields()
|
if (queryFormRef.value) {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
}
|
||||||
queryParams.pageNum = 1
|
queryParams.pageNum = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 行复选框选中
|
|
||||||
function handleSelectionChange(selection: any) {
|
|
||||||
ids.value = selection.map((item: any) => item.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onApiContactsExport = () => {
|
const onApiContactsExport = () => {
|
||||||
ApiContactsExport(queryParams).then((res: any) => {
|
ApiContactsExport(queryParams).then((res: any) => {
|
||||||
if (res.data.download_url) {
|
if (res.data.download_url) {
|
||||||
@@ -191,96 +170,125 @@ const onApiContactsExport = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 打开角色弹窗
|
|
||||||
// function handleOpenDialog(data: any = null) {
|
|
||||||
// dialog.visible = true
|
|
||||||
// if (data) {
|
|
||||||
// functionDialogBox(
|
|
||||||
// TaskForm,
|
|
||||||
// {
|
|
||||||
// newData: data
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: '编辑申请用印',
|
|
||||||
// width: '900',
|
|
||||||
// ok(value: any) {
|
|
||||||
// handleSubmit({ id: data.id, ...value })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// } else {
|
|
||||||
// functionDialogBox(
|
|
||||||
// TaskForm,
|
|
||||||
// {},
|
|
||||||
// {
|
|
||||||
// title: '提交新任务',
|
|
||||||
// width: '900',
|
|
||||||
// ok(value: any) {
|
|
||||||
// handleSubmit(value)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 提交角色表单
|
|
||||||
// function handleSubmit(data: any) {
|
|
||||||
// loading.value = true
|
|
||||||
// const roleId = data.id
|
|
||||||
// if (roleId) {
|
|
||||||
// BusinessEditApplication(data)
|
|
||||||
// .then(() => {
|
|
||||||
// ElMessage.success('修改成功')
|
|
||||||
// handleResetQuery()
|
|
||||||
// })
|
|
||||||
// .finally(() => (loading.value = false))
|
|
||||||
// } else {
|
|
||||||
// ApiTasksAdd(data)
|
|
||||||
// .then(() => {
|
|
||||||
// ElMessage.success('新增成功')
|
|
||||||
// handleResetQuery()
|
|
||||||
// })
|
|
||||||
// .finally(() => (loading.value = false))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const onUserDeleteDepartment = (id: string) => {
|
|
||||||
// ElMessageBox.confirm('确认删除已选中的数据项?', '警告', {
|
|
||||||
// confirmButtonText: '确定',
|
|
||||||
// cancelButtonText: '取消',
|
|
||||||
// type: 'warning'
|
|
||||||
// }).then(
|
|
||||||
// () => {
|
|
||||||
// loading.value = true
|
|
||||||
// BusinessDeleteApplication(id)
|
|
||||||
// .then(() => {
|
|
||||||
// ElMessage.success('删除成功')
|
|
||||||
// handleResetQuery()
|
|
||||||
// })
|
|
||||||
// .finally(() => (loading.value = false))
|
|
||||||
// },
|
|
||||||
// () => {
|
|
||||||
// ElMessage.info('已取消删除')
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const onDetails = (task_id: string) => {
|
|
||||||
// ApiTasksTaskId(task_id).then((res: any) => {
|
|
||||||
// functionDialogBox(
|
|
||||||
// TaskDetails,
|
|
||||||
// {
|
|
||||||
// newData: res.data
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: '详情',
|
|
||||||
// width: '900'
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleQuery()
|
handleQuery()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.contacts-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-panel,
|
||||||
|
.table-panel {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-panel {
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 18px 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-panel {
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b6472;
|
||||||
|
background: #f4f7fb;
|
||||||
|
border: 1px solid #e8eef7;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item--success {
|
||||||
|
background: rgba(103, 194, 58, 0.1);
|
||||||
|
color: #67c23a;
|
||||||
|
border-color: rgba(103, 194, 58, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item--wechat {
|
||||||
|
background: rgba(64, 158, 255, 0.1);
|
||||||
|
color: #409eff;
|
||||||
|
border-color: rgba(64, 158, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-table {
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f6f8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:hover > td) {
|
||||||
|
background: #f5f9ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
min-width: 62px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header__title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-panel__toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container ops-page">
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<div class="chart-title">每日数据明细</div>
|
<div class="chart-title">每日数据明细</div>
|
||||||
<el-radio-group v-model="queryParams.days" size="small" @change="handleTimeChange">
|
<el-radio-group v-model="queryParams.days" size="small" @change="handleTimeChange">
|
||||||
@@ -8,21 +8,19 @@
|
|||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<el-card shadow="hover" class="data-table">
|
<el-card shadow="hover" class="daily-table-card ops-card">
|
||||||
<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
|
<el-table
|
||||||
ref="dataTableRef"
|
ref="dataTableRef"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="roleList"
|
:data="roleList"
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
|
stripe
|
||||||
border
|
border
|
||||||
class="data-table__content"
|
class="daily-table ops-table"
|
||||||
|
empty-text="暂无统计数据"
|
||||||
>
|
>
|
||||||
<el-table-column label="联系人数" prop="contacts" />
|
<el-table-column label="联系人数" prop="contacts" />
|
||||||
|
<el-table-column label="打招呼人数" prop="greeted" />
|
||||||
<el-table-column label="回复人数" prop="replied" />
|
<el-table-column label="回复人数" prop="replied" />
|
||||||
<el-table-column label="微信相关数量" prop="wechat"></el-table-column>
|
<el-table-column label="微信相关数量" prop="wechat"></el-table-column>
|
||||||
<el-table-column label="回复率" prop="reply_rate" />
|
<el-table-column label="回复率" prop="reply_rate" />
|
||||||
@@ -115,85 +113,34 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<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 {
|
.chart-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-filter {
|
.daily-table-card {
|
||||||
display: flex;
|
border: 1px solid #ebeef5;
|
||||||
gap: 8px;
|
}
|
||||||
|
|
||||||
:deep(.el-button) {
|
:deep(.daily-table .el-table__header th) {
|
||||||
padding: 5px 12px;
|
background: #f8faff;
|
||||||
font-size: 13px;
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
&.el-button--primary {
|
:deep(.daily-table .el-table__row td) {
|
||||||
background-color: #409eff;
|
padding: 10px 0;
|
||||||
border-color: #409eff;
|
}
|
||||||
color: #fff;
|
|
||||||
}
|
:deep(.daily-table .el-table__row:hover > td) {
|
||||||
}
|
background-color: #f5f9ff !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const data = ref<any>({
|
|||||||
name: '联系人数',
|
name: '联系人数',
|
||||||
data: []
|
data: []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '打招呼',
|
||||||
|
data: []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '已回复',
|
name: '已回复',
|
||||||
data: []
|
data: []
|
||||||
@@ -54,6 +58,12 @@ const initChart = () => {
|
|||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'shadow'
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.96)',
|
||||||
|
borderColor: '#ebeef5',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#303133'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -86,8 +96,6 @@ const initChart = () => {
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
|
||||||
interval: 0.2,
|
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
@@ -109,6 +117,7 @@ const initChart = () => {
|
|||||||
series: data.value.series.map((item: any, index: number) => ({
|
series: data.value.series.map((item: any, index: number) => ({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
|
barMaxWidth: 26,
|
||||||
barGap: '10%',
|
barGap: '10%',
|
||||||
data: item.data,
|
data: item.data,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
@@ -119,7 +128,8 @@ const initChart = () => {
|
|||||||
itemStyle: {
|
itemStyle: {
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
animationDuration: 500
|
||||||
})),
|
})),
|
||||||
legend: {
|
legend: {
|
||||||
data: data.value.series.map((item: any) => item.name),
|
data: data.value.series.map((item: any) => item.name),
|
||||||
@@ -147,6 +157,10 @@ const onApiStatsDaily = (days: string) => {
|
|||||||
name: '联系人数',
|
name: '联系人数',
|
||||||
data: res.data?.map((item) => item.contacts)
|
data: res.data?.map((item) => item.contacts)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '打招呼',
|
||||||
|
data: res.data?.map((item) => item.greeted)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '已回复',
|
name: '已回复',
|
||||||
data: res.data?.map((item) => item.replied)
|
data: res.data?.map((item) => item.replied)
|
||||||
@@ -212,13 +226,15 @@ defineExpose({
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 12px 16px 8px;
|
||||||
background-color: #fff;
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-chart {
|
.trend-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: v-bind(height);
|
height: v-bind(height);
|
||||||
|
min-height: 320px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,50 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container stats-page ops-page">
|
||||||
<div class="headline-statistics">
|
<div class="stats-top">
|
||||||
<el-card shadow="hover" body-class="statistics-box" style="flex: 1">
|
<div class="stats-top__title">招聘数据概览</div>
|
||||||
<div class="statistics-box-img img-bg1">
|
<div class="stats-top__desc">实时追踪打招呼、回复、微信交换、Worker 与账号状态</div>
|
||||||
<el-icon><User /></el-icon>
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid" v-loading="statsLoading">
|
||||||
|
<el-card
|
||||||
|
v-for="card in statCards"
|
||||||
|
:key="card.key || card.label"
|
||||||
|
shadow="hover"
|
||||||
|
class="stats-card ops-card"
|
||||||
|
:class="[card.tone, { 'stats-card--active': card.key && activeMetric === card.key }]"
|
||||||
|
>
|
||||||
|
<div class="stats-card__icon">
|
||||||
|
<el-icon><component :is="card.icon" /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="statistics-box-text">
|
<div class="stats-card__content">
|
||||||
<span>总联系人数</span>
|
<p>{{ card.label }}</p>
|
||||||
<span>{{ apiStatsData.contacts?.total || 0 }}</span>
|
<h3>{{ card.value }}</h3>
|
||||||
<span>今日:{{ apiStatsData.contacts?.today || 0 }}</span>
|
<span>{{ card.sub }}</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>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<el-card shadow="hover">
|
|
||||||
<div class="chart-header">
|
<el-card shadow="hover" class="section-card ops-card">
|
||||||
<div class="chart-title">数据趋势</div>
|
<div class="section-card__header">
|
||||||
|
<div class="section-card__title">数据趋势</div>
|
||||||
<el-radio-group v-model="currentTime" size="small" @change="handleTimeChange">
|
<el-radio-group v-model="currentTime" size="small" @change="handleTimeChange">
|
||||||
<el-radio-button v-for="item in timeOptions" :key="item.value" :value="item.value">
|
<el-radio-button v-for="item in timeOptions" :key="item.value" :value="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
@@ -53,7 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<DataTrendChart ref="dataTrendChartRef" />
|
<DataTrendChart ref="dataTrendChartRef" />
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="hover">
|
|
||||||
|
<el-card shadow="hover" class="section-card ops-card">
|
||||||
<DailyDataBreakdown></DailyDataBreakdown>
|
<DailyDataBreakdown></DailyDataBreakdown>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +46,8 @@
|
|||||||
import { ApiStats } from '@/api/ContactInformation'
|
import { ApiStats } from '@/api/ContactInformation'
|
||||||
import { Monitor, User, ChatDotRound, ChatLineRound } from '@element-plus/icons-vue'
|
import { Monitor, User, ChatDotRound, ChatLineRound } from '@element-plus/icons-vue'
|
||||||
import DataTrendChart from './components/DataTrendChart.vue'
|
import DataTrendChart from './components/DataTrendChart.vue'
|
||||||
import { ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import DailyDataBreakdown from '@/views/DataStatistics/components/DailyDataBreakdown.vue'
|
import DailyDataBreakdown from '@/views/DataStatistics/components/DailyDataBreakdown.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -71,10 +55,10 @@ defineOptions({
|
|||||||
inheritAttrs: false
|
inheritAttrs: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// const queryFormRef = ref()
|
|
||||||
const dataTrendChartRef = ref()
|
const dataTrendChartRef = ref()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 数据
|
|
||||||
const currentTime = ref('today')
|
const currentTime = ref('today')
|
||||||
const timeOptions = [
|
const timeOptions = [
|
||||||
{ label: '今日', value: 'today' },
|
{ label: '今日', value: 'today' },
|
||||||
@@ -83,115 +67,262 @@ const timeOptions = [
|
|||||||
{ label: '全部', value: 'all' }
|
{ label: '全部', value: 'all' }
|
||||||
]
|
]
|
||||||
const apiStatsData = ref<any>({})
|
const apiStatsData = ref<any>({})
|
||||||
// // 弹窗
|
const statsLoading = ref(false)
|
||||||
// const dialog = reactive({
|
const activeMetric = ref('')
|
||||||
// title: '',
|
|
||||||
// visible: false
|
const METRIC_WHITELIST = ['contacts', 'greetings', 'replied', 'wechat', 'workers', 'accounts']
|
||||||
// })
|
const PERIOD_WHITELIST = ['today', 'week', 'month', 'all']
|
||||||
|
|
||||||
|
const normalizePeriod = (value: unknown) => {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
return PERIOD_WHITELIST.includes(text) ? text : 'today'
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveMetric = (value: unknown) => {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
return METRIC_WHITELIST.includes(text) ? text : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodToChartDays = (period: string) => {
|
||||||
|
if (period === 'today') return '1'
|
||||||
|
if (period === 'week') return '7'
|
||||||
|
if (period === 'month') return '30'
|
||||||
|
return '30'
|
||||||
|
}
|
||||||
|
|
||||||
// 获取数据
|
|
||||||
const onApiStats = () => {
|
const onApiStats = () => {
|
||||||
ApiStats({ period: currentTime.value }).then((res: any) => {
|
statsLoading.value = true
|
||||||
apiStatsData.value = res.data
|
ApiStats({ period: currentTime.value })
|
||||||
|
.then((res: any) => {
|
||||||
|
apiStatsData.value = res.data || {}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
statsLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const statCards = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'contacts',
|
||||||
|
label: '总联系人数',
|
||||||
|
value: apiStatsData.value?.contacts?.total || 0,
|
||||||
|
sub: `今日:${apiStatsData.value?.contacts?.today || 0}`,
|
||||||
|
icon: User,
|
||||||
|
tone: 'tone-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'greetings',
|
||||||
|
label: '打招呼人数',
|
||||||
|
value: apiStatsData.value?.greetings?.total || apiStatsData.value?.contacts?.greeted || 0,
|
||||||
|
sub: `今日:${apiStatsData.value?.greetings?.today || apiStatsData.value?.contacts?.today_greeted || 0}`,
|
||||||
|
icon: ChatDotRound,
|
||||||
|
tone: 'tone-2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'replied',
|
||||||
|
label: '已回复人数',
|
||||||
|
value: apiStatsData.value?.contacts?.replied || 0,
|
||||||
|
sub: `回复率:${apiStatsData.value?.contacts?.reply_rate || 0}%`,
|
||||||
|
icon: ChatDotRound,
|
||||||
|
tone: 'tone-5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wechat',
|
||||||
|
label: '微信交换数',
|
||||||
|
value: apiStatsData.value?.wechat?.total || 0,
|
||||||
|
sub: `成功率:${apiStatsData.value?.wechat?.success_rate || 0}%`,
|
||||||
|
icon: ChatLineRound,
|
||||||
|
tone: 'tone-3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'workers',
|
||||||
|
label: '在线Worker',
|
||||||
|
value: apiStatsData.value?.workers?.online || 0,
|
||||||
|
sub: `总数:${apiStatsData.value?.workers?.total || 0}`,
|
||||||
|
icon: Monitor,
|
||||||
|
tone: 'tone-4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'accounts',
|
||||||
|
label: '已登录账号数',
|
||||||
|
value: apiStatsData.value?.accounts?.logged_in || 0,
|
||||||
|
sub: `总数:${apiStatsData.value?.accounts?.total || 0}`,
|
||||||
|
icon: User,
|
||||||
|
tone: 'tone-6'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const syncFromRoute = () => {
|
||||||
|
currentTime.value = normalizePeriod(route.query.period)
|
||||||
|
activeMetric.value = resolveMetric(route.query.metric)
|
||||||
|
onApiStats()
|
||||||
|
dataTrendChartRef.value?.onApiStatsDaily(periodToChartDays(currentTime.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTimeChange = () => {
|
||||||
|
router.replace({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
period: currentTime.value
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 重置查询
|
|
||||||
// function handleResetQuery() {
|
|
||||||
// if (queryFormRef.value) queryFormRef.value?.resetFields()
|
|
||||||
// queryParams.pageNum = 1
|
|
||||||
// fetchData()
|
|
||||||
// }
|
|
||||||
|
|
||||||
const handleTimeChange = () => {
|
|
||||||
onApiStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
onApiStats()
|
syncFromRoute()
|
||||||
dataTrendChartRef.value?.onApiStatsDaily('7')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [route.query.period, route.query.metric],
|
||||||
|
() => syncFromRoute()
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.headline-statistics {
|
.stats-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
flex-direction: column;
|
||||||
margin-bottom: 16px;
|
gap: 16px;
|
||||||
height: 140px;
|
|
||||||
}
|
}
|
||||||
::v-deep(.statistics-box) {
|
|
||||||
|
.stats-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
//justify-content: center;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
color: #fff;
|
|
||||||
font-size: 30px;
|
&__title {
|
||||||
gap: 20px;
|
font-size: 22px;
|
||||||
.statistics-box-img {
|
font-weight: 700;
|
||||||
width: 70px;
|
color: #1f2d3d;
|
||||||
height: 70px;
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 15px;
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
.img-bg1 {
|
|
||||||
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
|
&__content {
|
||||||
}
|
p {
|
||||||
.img-bg2 {
|
margin: 0;
|
||||||
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
|
font-size: 13px;
|
||||||
}
|
color: #909399;
|
||||||
.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;
|
h3 {
|
||||||
color: #000;
|
margin: 6px 0 4px;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #111827;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
& > span:nth-child(3) {
|
|
||||||
|
span {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chart-header {
|
|
||||||
display: flex;
|
.stats-card--active {
|
||||||
justify-content: space-between;
|
border-color: #409eff;
|
||||||
align-items: center;
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.12);
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.tone-1 .stats-card__icon {
|
||||||
font-size: 20px;
|
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-filter {
|
.tone-2 .stats-card__icon {
|
||||||
display: flex;
|
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
|
||||||
gap: 8px;
|
}
|
||||||
|
|
||||||
:deep(.el-button) {
|
.tone-3 .stats-card__icon {
|
||||||
padding: 5px 12px;
|
background: linear-gradient(135deg, #56bafd 0%, #16e9fd 100%);
|
||||||
font-size: 13px;
|
}
|
||||||
|
|
||||||
&.el-button--primary {
|
.tone-4 .stats-card__icon {
|
||||||
background-color: #409eff;
|
background: linear-gradient(135deg, #51eb90 0%, #49f7d2 100%);
|
||||||
border-color: #409eff;
|
}
|
||||||
color: #fff;
|
|
||||||
|
.tone-5 .stats-card__icon {
|
||||||
|
background: linear-gradient(135deg, #a18cf5 0%, #7d7df5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-6 .stats-card__icon {
|
||||||
|
background: linear-gradient(135deg, #36d1dc 0%, #5b86e5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-top__title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__content h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card__header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,93 +1,154 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container ops-page">
|
||||||
<el-card shadow="never" class="mt-2">
|
<el-card shadow="hover" class="mt-2 hero-card ops-card">
|
||||||
<div class="flex flex-wrap">
|
<div class="hero-panel">
|
||||||
<!-- 左侧问候语区域 -->
|
<div class="hero-left">
|
||||||
<div class="flex-1 flex items-start">
|
<p class="hero-greeting">{{ greetings }}</p>
|
||||||
<!-- <img class="w80px h80px rounded-full" src="../../assets/user.png" />-->
|
<div class="hero-propaganda">
|
||||||
<div class="ml-5">
|
<div
|
||||||
<p>{{ greetings }}</p>
|
v-if="publicityMaterials"
|
||||||
<div style="width: 100%">
|
class="hero-propaganda__text"
|
||||||
<div
|
@click="userStore.checkPermission('HomePage:propagandaEit:editor') && onPropagandaEit()"
|
||||||
v-if="publicityMaterials"
|
>
|
||||||
style="height: 20px; margin: 0 0 14px 0"
|
<p>{{ userStore.userInfo?.content || '点击设置展示文案' }}</p>
|
||||||
@click="
|
|
||||||
userStore.checkPermission('HomePage:propagandaEit:editor') && onPropagandaEit()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p class="text-sm text-gray">
|
|
||||||
{{ userStore.userInfo?.content }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<el-input
|
|
||||||
v-else
|
|
||||||
v-model="propagandaEitValue"
|
|
||||||
placeholder="请输入"
|
|
||||||
style="width: 240px; height: 20px; margin: 0 0 14px 0"
|
|
||||||
class="mt-2"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleQuery"
|
|
||||||
@blur="handleQuery"
|
|
||||||
></el-input>
|
|
||||||
</div>
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="propagandaEitValue"
|
||||||
|
placeholder="请输入首页展示文案"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
@blur="handleQuery"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧图标区域 - PC端 -->
|
|
||||||
<div class="hidden sm:block" style="display: flex; gap: 20px">
|
<div class="hero-right">
|
||||||
<!-- 快捷入口 -->
|
<div class="quick-header">
|
||||||
<div
|
<div class="quick-header__title">快捷入口</div>
|
||||||
ref="quickScrollRef"
|
<div class="quick-header__meta ops-actions">
|
||||||
class="flex w-700px gap-2 overflow-hidden quick"
|
<el-tag type="info" effect="light">已启用 {{ quickData.length }} 项</el-tag>
|
||||||
@wheel="handleWheel"
|
<el-popover placement="left-start" width="300">
|
||||||
>
|
<template #reference>
|
||||||
|
<el-button circle class="quick-config-btn ops-btn" title="快捷键配置">
|
||||||
|
<el-icon><Operation /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
:props="{
|
||||||
|
label: 'title'
|
||||||
|
}"
|
||||||
|
node-key="name"
|
||||||
|
:default-checked-keys="defaultCheckedKeys"
|
||||||
|
:data="quickConfigurationList"
|
||||||
|
show-checkbox
|
||||||
|
@check-change="handleCheckChange"
|
||||||
|
/>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="quickScrollRef" class="quick-list quick" @wheel="handleWheel">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in quickData"
|
v-for="(item, index) in quickData"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex-1 flex items-center justify-center flex-col cursor-pointer"
|
class="quick-item"
|
||||||
@click="navigateTo(item.name)"
|
@click="navigateTo(item.name)"
|
||||||
>
|
>
|
||||||
<el-icon class="w-12 h-12 text-primary"><Plus /></el-icon>
|
<el-icon class="quick-item__icon"><Plus /></el-icon>
|
||||||
<el-text type="primary" size="small" style="white-space: nowrap">
|
<el-text type="primary" size="small" class="quick-item__text">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</el-text>
|
</el-text>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!quickData.length" class="quick-empty">请先在右上角配置快捷入口</div>
|
||||||
</div>
|
</div>
|
||||||
<el-popover placement="right-start" width="300">
|
|
||||||
<template #reference>
|
|
||||||
<el-icon title="快捷键配置"><Operation /></el-icon>
|
|
||||||
</template>
|
|
||||||
<el-tree
|
|
||||||
ref="treeRef"
|
|
||||||
style="max-width: 600px"
|
|
||||||
:props="{
|
|
||||||
label: 'title'
|
|
||||||
}"
|
|
||||||
node-key="name"
|
|
||||||
:default-checked-keys="defaultCheckedKeys"
|
|
||||||
:data="quickConfigurationList"
|
|
||||||
show-checkbox
|
|
||||||
@check-change="handleCheckChange"
|
|
||||||
/>
|
|
||||||
</el-popover>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 数据统计 -->
|
<!-- 数据统计 -->
|
||||||
<el-row :gutter="10" class="mt-5">
|
<div class="section-title ops-section-title mt-5">招聘运营概览</div>
|
||||||
<!-- 在线用户数量 -->
|
<el-row :gutter="10" class="mt-3">
|
||||||
|
<el-col :span="6" :xs="12" class="mb-xs-3">
|
||||||
|
<el-card
|
||||||
|
shadow="never"
|
||||||
|
class="stat-card stat-card--clickable ops-card"
|
||||||
|
@click="goToDataStatistics('contacts')"
|
||||||
|
>
|
||||||
|
<div class="stat-card__icon stat-card__icon--1">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card__content">
|
||||||
|
<p>联系人数</p>
|
||||||
|
<h3>{{ recruitStats.contacts?.total || 0 }}</h3>
|
||||||
|
<span>今日:{{ recruitStats.contacts?.today || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6" :xs="12" class="mb-xs-3">
|
||||||
|
<el-card
|
||||||
|
shadow="never"
|
||||||
|
class="stat-card stat-card--clickable ops-card"
|
||||||
|
@click="goToDataStatistics('greetings')"
|
||||||
|
>
|
||||||
|
<div class="stat-card__icon stat-card__icon--2">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card__content">
|
||||||
|
<p>打招呼人数</p>
|
||||||
|
<h3>{{ recruitStats.greetings?.total || recruitStats.contacts?.greeted || 0 }}</h3>
|
||||||
|
<span>今日:{{ recruitStats.greetings?.today || recruitStats.contacts?.today_greeted || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6" :xs="12" class="mb-xs-3">
|
||||||
|
<el-card
|
||||||
|
shadow="never"
|
||||||
|
class="stat-card stat-card--clickable ops-card"
|
||||||
|
@click="goToDataStatistics('replied')"
|
||||||
|
>
|
||||||
|
<div class="stat-card__icon stat-card__icon--3">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card__content">
|
||||||
|
<p>已回复人数</p>
|
||||||
|
<h3>{{ recruitStats.contacts?.replied || 0 }}</h3>
|
||||||
|
<span>回复率:{{ recruitStats.contacts?.reply_rate || 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6" :xs="12" class="mb-xs-3">
|
||||||
|
<el-card
|
||||||
|
shadow="never"
|
||||||
|
class="stat-card stat-card--clickable ops-card"
|
||||||
|
@click="goToDataStatistics('wechat')"
|
||||||
|
>
|
||||||
|
<div class="stat-card__icon stat-card__icon--4">
|
||||||
|
<el-icon><ChatLineRound /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card__content">
|
||||||
|
<p>微信交换数</p>
|
||||||
|
<h3>{{ recruitStats.wechat?.total || 0 }}</h3>
|
||||||
|
<span>成功率:{{ recruitStats.wechat?.success_rate || 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div class="section-title ops-section-title mt-5">消息与日程</div>
|
||||||
|
<el-row :gutter="12" class="mt-3">
|
||||||
<el-col :span="12" :xs="24" class="mb-xs-3">
|
<el-col :span="12" :xs="24" class="mb-xs-3">
|
||||||
<el-card shadow="never" class="h-full flex flex-col">
|
<el-card shadow="hover" class="panel-card ops-card h-full">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex-x-between">
|
<div class="panel-card__header">
|
||||||
<el-badge
|
<el-badge
|
||||||
v-if="userRoxyexhibitionList?.length > 0"
|
v-if="userRoxyexhibitionList?.length > 0"
|
||||||
:value="userRoxyexhibitionTotal"
|
:value="userRoxyexhibitionTotal"
|
||||||
:max="99"
|
:max="99"
|
||||||
>
|
>
|
||||||
<span>代办事项</span>
|
<span class="panel-card__title">待办事项</span>
|
||||||
</el-badge>
|
</el-badge>
|
||||||
<span v-else>代办事项</span>
|
<span v-else class="panel-card__title">待办事项</span>
|
||||||
<el-link type="primary" underline="never" @click="handleViewMoreNotice">
|
<el-link type="primary" underline="never" @click="handleViewMoreNotice">
|
||||||
<span class="text-xs">查看更多</span>
|
<span class="text-xs">查看更多</span>
|
||||||
<el-icon class="text-xs">
|
<el-icon class="text-xs">
|
||||||
@@ -96,21 +157,15 @@
|
|||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div style="height: 150px; overflow: hidden; overflow-y: auto">
|
<div class="panel-list panel-list--short">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in userRoxyexhibitionList"
|
v-for="(item, index) in userRoxyexhibitionList"
|
||||||
:key="index"
|
:key="index"
|
||||||
style="
|
class="panel-item panel-item--clickable"
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 5px;
|
|
||||||
|
|
||||||
gap: 10px;
|
|
||||||
"
|
|
||||||
@click="handleReadNotice(item)"
|
@click="handleReadNotice(item)"
|
||||||
>
|
>
|
||||||
<el-tag>{{ item.type }}</el-tag>
|
<el-tag effect="light" class="ops-status-tag">{{ item.type }}</el-tag>
|
||||||
<el-text>{{ item.title }}</el-text>
|
<el-text class="panel-item__title">{{ item.title }}</el-text>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
effect="light"
|
effect="light"
|
||||||
@@ -118,53 +173,40 @@
|
|||||||
:content="item.content"
|
:content="item.content"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<el-text
|
<el-text type="info" class="panel-item__content">{{ item.content }}</el-text>
|
||||||
type="info"
|
|
||||||
style="flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
|
||||||
>
|
|
||||||
{{ item.content }}
|
|
||||||
</el-text>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!userRoxyexhibitionList?.length" class="list-empty">暂无待办事项</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<!-- 浏览量(PV) -->
|
|
||||||
<el-col :span="12" :xs="24">
|
<el-col :span="12" :xs="24" class="mb-xs-3">
|
||||||
<el-card>
|
<el-card shadow="hover" class="panel-card ops-card h-full">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div>重要日程提示</div>
|
<div class="panel-card__header">
|
||||||
|
<span class="panel-card__title">重要日程提示</span>
|
||||||
|
<el-tag effect="light" type="info">共 {{ businessScheduleDetailList?.length || 0 }} 条</el-tag>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div class="panel-list panel-list--short panel-list--column">
|
||||||
style="
|
|
||||||
height: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
gap: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in businessScheduleDetailList"
|
v-for="(item, index) in businessScheduleDetailList"
|
||||||
:key="index"
|
:key="index"
|
||||||
style="
|
:class="[
|
||||||
display: flex;
|
'panel-item',
|
||||||
align-items: center;
|
'schedule-item',
|
||||||
padding: 10px 10px;
|
{
|
||||||
gap: 10px;
|
'schedule-item--danger': !(item.state === '已完成' || isCurrentTimeLessThan(item.end_time)),
|
||||||
border-radius: 8px;
|
'blink-animation': !(item.state === '已完成' || isCurrentTimeLessThan(item.end_time))
|
||||||
"
|
}
|
||||||
:class="{
|
]"
|
||||||
'blink-animation': !(
|
|
||||||
item.state === '已完成' || isCurrentTimeLessThan(item.end_time)
|
|
||||||
)
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<el-text
|
<el-text
|
||||||
:type="
|
:type="
|
||||||
item.state === '已完成' || isCurrentTimeLessThan(item.end_time) ? '' : 'danger'
|
item.state === '已完成' || isCurrentTimeLessThan(item.end_time) ? '' : 'danger'
|
||||||
"
|
"
|
||||||
|
class="panel-item__title"
|
||||||
>
|
>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</el-text>
|
</el-text>
|
||||||
@@ -175,38 +217,33 @@
|
|||||||
:content="item.remark"
|
:content="item.remark"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<el-text
|
<el-text type="info" class="panel-item__content">{{ item.remark }}</el-text>
|
||||||
type="info"
|
|
||||||
style="flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
|
||||||
>
|
|
||||||
{{ item.remark }}
|
|
||||||
</el-text>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<span>
|
<el-tag type="info" effect="light" size="small" class="ops-status-tag">{{ item.tiems }}</el-tag>
|
||||||
<el-tag type="info">{{ item.tiems }}</el-tag>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!businessScheduleDetailList?.length" class="list-empty">暂无日程数据</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="10" class="mt-5">
|
<el-row :gutter="12" class="mt-5">
|
||||||
<el-col :xs="24" :span="12">
|
<el-col :xs="24" :span="12" class="mb-xs-3">
|
||||||
<el-card>
|
<el-card shadow="hover" class="panel-card ops-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex-x-between">
|
<div class="panel-card__header">
|
||||||
<span>通知/公示</span>
|
<span class="panel-card__title">通知 / 公示</span>
|
||||||
|
<el-tag effect="light" type="info">共 {{ businessBulletindetailList?.length || 0 }} 条</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div style="height: 490px; overflow: hidden; overflow-y: auto">
|
<div class="panel-list panel-list--tall">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in businessBulletindetailList"
|
v-for="(item, index) in businessBulletindetailList"
|
||||||
:key="index"
|
:key="index"
|
||||||
style="display: flex; align-items: center; padding: 10px 5px; gap: 10px"
|
class="panel-item panel-item--clickable"
|
||||||
@click="businessBulletindetailClick(item)"
|
@click="businessBulletindetailClick(item)"
|
||||||
>
|
>
|
||||||
<el-text>{{ item.title }}</el-text>
|
<el-text class="panel-item__title">{{ item.title }}</el-text>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
effect="light"
|
effect="light"
|
||||||
@@ -214,35 +251,30 @@
|
|||||||
:content="item.content"
|
:content="item.content"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<el-text
|
<el-text type="info" class="panel-item__content">{{ item.content }}</el-text>
|
||||||
type="info"
|
|
||||||
style="flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
|
||||||
>
|
|
||||||
{{ item.content }}
|
|
||||||
</el-text>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<span>
|
<el-tag type="info" effect="light" size="small" class="ops-status-tag">{{ item.times }}</el-tag>
|
||||||
<el-tag type="info">{{ item.times }}</el-tag>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!businessBulletindetailList?.length" class="list-empty">暂无通知数据</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :span="12">
|
<el-col :xs="24" :span="12">
|
||||||
<el-card>
|
<el-card shadow="hover" class="panel-card ops-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex-x-between">
|
<div class="panel-card__header">
|
||||||
<span>制度公示</span>
|
<span class="panel-card__title">制度公示</span>
|
||||||
|
<el-tag effect="light" type="info">共 {{ regulationDisclosureList?.length || 0 }} 条</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div style="height: 490px; overflow: hidden; overflow-y: auto">
|
<div class="panel-list panel-list--tall">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in regulationDisclosureList"
|
v-for="(item, index) in regulationDisclosureList"
|
||||||
:key="index"
|
:key="index"
|
||||||
style="display: flex; align-items: center; padding: 10px 5px; gap: 10px"
|
class="panel-item panel-item--clickable"
|
||||||
@click="businessBulletindetailClick(item)"
|
@click="businessBulletindetailClick(item)"
|
||||||
>
|
>
|
||||||
<el-text>{{ item.title }}</el-text>
|
<el-text class="panel-item__title">{{ item.title }}</el-text>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
effect="light"
|
effect="light"
|
||||||
@@ -250,17 +282,11 @@
|
|||||||
:content="item.content"
|
:content="item.content"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<el-text
|
<el-text type="info" class="panel-item__content">{{ item.content }}</el-text>
|
||||||
type="info"
|
|
||||||
style="flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
|
||||||
>
|
|
||||||
{{ item.content }}
|
|
||||||
</el-text>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<span>
|
<el-tag type="info" effect="light" size="small" class="ops-status-tag">{{ item.times }}</el-tag>
|
||||||
<el-tag type="info">{{ item.times }}</el-tag>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!regulationDisclosureList?.length" class="list-empty">暂无制度公示</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -274,6 +300,7 @@ import {
|
|||||||
UserApprovalStatusCheck,
|
UserApprovalStatusCheck,
|
||||||
UserRoxyexhibition
|
UserRoxyexhibition
|
||||||
} from '@/api/calibration/approval'
|
} from '@/api/calibration/approval'
|
||||||
|
import { ApiStats } from '@/api/ContactInformation'
|
||||||
// import ImageIconComponent from '@/components/ImageIconComponent/index.vue'
|
// import ImageIconComponent from '@/components/ImageIconComponent/index.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -282,7 +309,7 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
import { useUserStore } from '@/store/modules/user-store'
|
import { useUserStore } from '@/store/modules/user-store'
|
||||||
import { Plus, Operation, ArrowRight } from '@element-plus/icons-vue'
|
import { Plus, Operation, ArrowRight, User, ChatDotRound, ChatLineRound, Message } from '@element-plus/icons-vue'
|
||||||
import { BusinessScheduleDetail } from '@/api/calibration/lmportantScheduleManagement'
|
import { BusinessScheduleDetail } from '@/api/calibration/lmportantScheduleManagement'
|
||||||
import { BusinessBulletindetail } from '@/api/calibration/announcementManagement'
|
import { BusinessBulletindetail } from '@/api/calibration/announcementManagement'
|
||||||
import { BusinessLawdisplay } from '@/api/calibration/lawFirmStandardDocuments'
|
import { BusinessLawdisplay } from '@/api/calibration/lawFirmStandardDocuments'
|
||||||
@@ -314,6 +341,7 @@ const businessScheduleDetailList = ref<any[]>([])
|
|||||||
const businessBulletindetailList = ref<any[]>([])
|
const businessBulletindetailList = ref<any[]>([])
|
||||||
const regulationDisclosureList = ref<any[]>([])
|
const regulationDisclosureList = ref<any[]>([])
|
||||||
const businessLawdisplayList = ref<any[]>([])
|
const businessLawdisplayList = ref<any[]>([])
|
||||||
|
const recruitStats = ref<any>({})
|
||||||
// 当前时间(用于计算问候语)
|
// 当前时间(用于计算问候语)
|
||||||
const currentDate = new Date()
|
const currentDate = new Date()
|
||||||
|
|
||||||
@@ -340,6 +368,16 @@ const navigateTo = (name: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToDataStatistics = (metric: 'contacts' | 'greetings' | 'replied' | 'wechat') => {
|
||||||
|
router.push({
|
||||||
|
name: 'DataStatistics',
|
||||||
|
query: {
|
||||||
|
period: 'all',
|
||||||
|
metric
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onUserRoxyexhibition = () => {
|
const onUserRoxyexhibition = () => {
|
||||||
UserRoxyexhibition({
|
UserRoxyexhibition({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
@@ -384,6 +422,12 @@ const onBusinessLawdisplay = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onRecruitStats = () => {
|
||||||
|
ApiStats({ period: 'all' }).then((res: any) => {
|
||||||
|
recruitStats.value = res.data || {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleCheckChange = () => {
|
const handleCheckChange = () => {
|
||||||
treeRef.value?.getCheckedNodes()
|
treeRef.value?.getCheckedNodes()
|
||||||
quickData.value = treeRef.value?.getCheckedNodes()
|
quickData.value = treeRef.value?.getCheckedNodes()
|
||||||
@@ -511,6 +555,7 @@ const businessBulletindetailClick = (data: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
onRecruitStats()
|
||||||
onUserRoxyexhibition()
|
onUserRoxyexhibition()
|
||||||
onBusinessScheduleDetail()
|
onBusinessScheduleDetail()
|
||||||
onBusinessBulletindetail()
|
onBusinessBulletindetail()
|
||||||
@@ -554,6 +599,329 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 18px rgba(64, 158, 255, 0.12);
|
||||||
|
border-color: #d9ecff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--1 {
|
||||||
|
background: linear-gradient(135deg, #7f7fd5 0%, #9156e8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--2 {
|
||||||
|
background: linear-gradient(135deg, #f199ee 0%, #ef6b8b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--3 {
|
||||||
|
background: linear-gradient(135deg, #56bafd 0%, #16e9fd 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--4 {
|
||||||
|
background: linear-gradient(135deg, #51eb90 0%, #49f7d2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-panel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-greeting {
|
||||||
|
margin: 0;
|
||||||
|
color: #1f2d3d;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-propaganda {
|
||||||
|
max-width: 440px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #d8dee8;
|
||||||
|
background: #f7f9fc;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #616f85;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #b8d6ff;
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-right {
|
||||||
|
width: 54%;
|
||||||
|
min-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-config-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
color: #409eff;
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 98px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-item {
|
||||||
|
min-width: 96px;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #e8edf5;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f9fbfe;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: #b8d6ff;
|
||||||
|
background: #eef5ff;
|
||||||
|
box-shadow: 0 8px 16px rgba(64, 158, 255, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-item__icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: #409eff;
|
||||||
|
background: rgba(64, 158, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-item__text {
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-empty {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 90px;
|
||||||
|
border: 1px dashed #d8dee8;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #909399;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list--short {
|
||||||
|
height: 176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list--tall {
|
||||||
|
height: 490px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list--column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f9ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item__title {
|
||||||
|
max-width: 170px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item__content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-item {
|
||||||
|
border: 1px solid #f2f3f5;
|
||||||
|
background: #fafbfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-item--danger {
|
||||||
|
border-color: rgba(245, 108, 108, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-empty {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 88px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.quick {
|
.quick {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
@@ -585,4 +953,31 @@ onMounted(() => {
|
|||||||
.blink-animation {
|
.blink-animation {
|
||||||
animation: blinkBackground 1s infinite;
|
animation: blinkBackground 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-panel {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-right {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-greeting {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-item {
|
||||||
|
min-width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list--tall {
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item__title {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user