1
This commit is contained in:
130
admin/src/api/README.md
Normal file
130
admin/src/api/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# API 服务目录结构
|
||||
|
||||
## 📁 优化后的目录结构
|
||||
|
||||
根据菜单模块聚合优化,API 服务按照以下结构组织:
|
||||
|
||||
```
|
||||
api/
|
||||
├── work/ # 工作管理模块
|
||||
│ ├── apply_records_server.js # 投递记录 API
|
||||
│ └── job_postings_server.js # 岗位信息 API
|
||||
├── profile/ # 账号简历模块
|
||||
│ ├── pla_account_server.js # 平台账号 API
|
||||
│ └── resume_info_server.js # 简历信息 API
|
||||
├── device/ # 设备监控模块
|
||||
│ └── device_status_server.js # 设备状态 API
|
||||
├── operation/ # 任务聊天模块
|
||||
│ ├── task_status_server.js # 任务状态 API
|
||||
│ └── chat_records_server.js # 聊天记录 API
|
||||
└── system/ # 系统设置模块
|
||||
└── system_config_server.js # 系统配置 API
|
||||
```
|
||||
|
||||
## 🔄 模块映射关系
|
||||
|
||||
| 新模块 | 原模块 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `work/` | `job/` | 工作管理 = 岗位 + 投递 |
|
||||
| `profile/` | `account/` | 账号简历 = 账号 + 简历 |
|
||||
| `device/` | `device/` | 设备监控(保持不变) |
|
||||
| `operation/` | `task/` + `chat/` | 任务聊天 = 任务 + 聊天 |
|
||||
| `system/` | `system/` | 系统设置(保持不变) |
|
||||
|
||||
## 📝 API 服务文件说明
|
||||
|
||||
### 1. work/apply_records_server.js
|
||||
投递记录 API 服务
|
||||
- `page(param)` - 分页查询投递记录
|
||||
- `getStatistics()` - 获取投递统计数据
|
||||
- `getById(id)` - 获取单条记录详情
|
||||
- `del(row)` - 删除投递记录
|
||||
- `batchDelete(ids)` - 批量删除
|
||||
|
||||
### 2. work/job_postings_server.js
|
||||
岗位信息 API 服务
|
||||
- `page(param)` - 分页查询岗位信息
|
||||
- `getStatistics()` - 获取岗位统计数据
|
||||
- `getById(id)` - 获取单条岗位详情
|
||||
- `del(row)` - 删除岗位信息
|
||||
- `batchDelete(ids)` - 批量删除
|
||||
|
||||
### 3. profile/pla_account_server.js
|
||||
平台账号 API 服务
|
||||
- `page(param)` - 分页查询平台账号
|
||||
- `getStatistics()` - 获取账号统计数据
|
||||
- `getById(id)` - 获取单条账号详情
|
||||
- `update(row)` - 更新账号信息
|
||||
- `del(row)` - 删除账号
|
||||
|
||||
### 4. profile/resume_info_server.js
|
||||
简历信息 API 服务
|
||||
- `page(param)` - 分页查询简历信息
|
||||
- `getStatistics()` - 获取简历统计数据
|
||||
- `getById(id)` - 获取单条简历详情
|
||||
- `del(row)` - 删除简历
|
||||
|
||||
### 5. device/device_status_server.js
|
||||
设备状态 API 服务
|
||||
- `page(param)` - 分页查询设备状态
|
||||
- `getOverview()` - 获取设备概览统计
|
||||
- `updateConfig(data)` - 更新设备配置
|
||||
- `resetError(deviceSn)` - 重置设备错误
|
||||
- `del(row)` - 删除设备记录
|
||||
|
||||
### 6. operation/task_status_server.js
|
||||
任务状态 API 服务
|
||||
- `page(param)` - 分页查询任务状态
|
||||
- `getStatistics()` - 获取任务统计数据
|
||||
- `getById(id)` - 获取单条任务详情
|
||||
- `update(row)` - 更新任务状态
|
||||
- `del(row)` - 删除任务
|
||||
|
||||
### 7. operation/chat_records_server.js
|
||||
聊天记录 API 服务
|
||||
- `page(param)` - 分页查询聊天记录
|
||||
- `getStatistics()` - 获取聊天统计数据
|
||||
- `getById(id)` - 获取单条聊天详情
|
||||
- `del(row)` - 删除聊天记录
|
||||
|
||||
### 8. system/system_config_server.js
|
||||
系统配置 API 服务
|
||||
- `page(param)` - 分页查询系统配置
|
||||
- `getCategories()` - 获取配置分类列表
|
||||
- `get(key)` - 获取单条配置详情
|
||||
- `add(data)` - 添加配置
|
||||
- `update(row)` - 更新配置
|
||||
- `batchUpdate(configs)` - 批量更新配置
|
||||
- `del(row)` - 删除配置
|
||||
- `reset(key)` - 重置配置为默认值
|
||||
|
||||
## 🎯 使用示例
|
||||
|
||||
```javascript
|
||||
// 导入 API 服务
|
||||
import applyRecordsServer from '@/api/work/apply_records_server'
|
||||
import deviceStatusServer from '@/api/device/device_status_server'
|
||||
|
||||
// 查询投递记录
|
||||
const result = await applyRecordsServer.page({
|
||||
seachOption: { applyStatus: 'success' },
|
||||
pageOption: { page: 1, pageSize: 20 }
|
||||
})
|
||||
|
||||
// 获取设备概览
|
||||
const overview = await deviceStatusServer.getOverview()
|
||||
```
|
||||
|
||||
## 📌 注意事项
|
||||
|
||||
1. 所有 API 服务都已配置为调用 `/admin_api/*` 路径的后端接口
|
||||
2. 使用 `window.framework.http` 进行 HTTP 请求
|
||||
3. 分页查询统一使用 `page(param)` 方法,参数包含 `seachOption` 和 `pageOption`
|
||||
4. 所有服务类都已实例化并导出,可直接使用
|
||||
|
||||
## ✅ 迁移完成
|
||||
|
||||
- ✅ 所有 API 服务文件已按新目录结构创建
|
||||
- ✅ 所有接口路径已更新为 `/admin_api/*` 格式
|
||||
- ✅ 目录结构与 SQL 菜单定义保持一致
|
||||
|
||||
81
admin/src/api/chat/chat_records_server.js
Normal file
81
admin/src/api/chat/chat_records_server.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 聊天记录 API 服务
|
||||
*/
|
||||
|
||||
class ChatRecordsServer {
|
||||
/**
|
||||
* 分页查询聊天记录
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('chat/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录详情
|
||||
* @param {String} chatId - 聊天记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(chatId) {
|
||||
return window.framework.http.post('chat/detail', { chatId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天统计
|
||||
* @returns {Promise}
|
||||
*/
|
||||
statistics() {
|
||||
return window.framework.http.get('chat/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天记录
|
||||
* @param {Object} data - 记录数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(data) {
|
||||
return window.framework.http.post('chat_records', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新聊天记录
|
||||
* @param {Object} row - 记录数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.put(`chat_records/${row.chatId}`, row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天记录
|
||||
* @param {Object} row - 记录数据(包含chatId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('chat/delete', { chatId: row.chatId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('chat_records/batch_delete', { ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('chat/export', param, { responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChatRecordsServer()
|
||||
|
||||
62
admin/src/api/device/device_status_server.js
Normal file
62
admin/src/api/device/device_status_server.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 设备状态 API 服务
|
||||
*/
|
||||
|
||||
class DeviceStatusServer {
|
||||
/**
|
||||
* 分页查询设备状态
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/device/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备详情
|
||||
* @param {String} deviceSn - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(deviceSn) {
|
||||
return window.framework.http.post('/device/detail', { deviceSn })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备概览统计
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getOverview() {
|
||||
return window.framework.http.get('/device/overview')
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备配置
|
||||
* @param {Object} data - 设备配置数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateConfig(data) {
|
||||
return window.framework.http.post('/device/update-config', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置设备错误
|
||||
* @param {String} deviceSn - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
resetError(deviceSn) {
|
||||
return window.framework.http.post('/device/reset-error', { deviceSn })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备记录
|
||||
* @param {Object} row - 设备数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/device/delete', { deviceSn: row.deviceSn })
|
||||
}
|
||||
}
|
||||
|
||||
export default new DeviceStatusServer()
|
||||
81
admin/src/api/job/apply_records_server.js
Normal file
81
admin/src/api/job/apply_records_server.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 投递记录 API 服务
|
||||
*/
|
||||
|
||||
class ApplyRecordsServer {
|
||||
/**
|
||||
* 分页查询投递记录
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('apply/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录详情
|
||||
* @param {String} applyId - 投递记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(applyId) {
|
||||
return window.framework.http.post('apply/detail', { applyId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取投递统计
|
||||
* @returns {Promise}
|
||||
*/
|
||||
statistics() {
|
||||
return window.framework.http.get('apply/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增投递记录
|
||||
* @param {Object} data - 记录数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(data) {
|
||||
return window.framework.http.post('apply_records', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新投递记录
|
||||
* @param {Object} row - 记录数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.put(`apply_records/${row.applyId}`, row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除投递记录
|
||||
* @param {Object} row - 记录数据(包含applyId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('apply/delete', { applyId: row.applyId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('apply_records/batch_delete', { ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('apply/export', param, { responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
|
||||
export default new ApplyRecordsServer()
|
||||
|
||||
95
admin/src/api/job/job_postings_server.js
Normal file
95
admin/src/api/job/job_postings_server.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 岗位信息 API 服务
|
||||
*/
|
||||
|
||||
class JobPostingsServer {
|
||||
/**
|
||||
* 分页查询岗位信息
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('job/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录详情
|
||||
* @param {String} jobId - 岗位ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(jobId) {
|
||||
return window.framework.http.post('job/detail', { jobId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位统计
|
||||
* @returns {Promise}
|
||||
*/
|
||||
statistics() {
|
||||
return window.framework.http.get('job/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 职位打招呼
|
||||
* @param {Object} data - 打招呼参数
|
||||
* @param {String} data.sn_code - 设备SN码
|
||||
* @param {String} data.encryptJobId - 加密的职位ID
|
||||
* @param {String} data.securityId - 安全ID(可选)
|
||||
* @param {String} data.brandName - 公司名称
|
||||
* @param {String} data.platform - 平台(默认boss)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
greet(data) {
|
||||
return window.framework.http.post('job/greet', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增岗位信息
|
||||
* @param {Object} data - 记录数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(data) {
|
||||
return window.framework.http.post('job_postings', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新岗位信息
|
||||
* @param {Object} row - 记录数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.put(`job_postings/${row.jobId}`, row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除岗位信息
|
||||
* @param {Object} row - 记录数据(包含jobId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('job/delete', { jobId: row.jobId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('job_postings/batch_delete', { ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('job/export', param, { responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
|
||||
export default new JobPostingsServer()
|
||||
|
||||
110
admin/src/api/operation/chat_records_server.js
Normal file
110
admin/src/api/operation/chat_records_server.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 聊天记录 API 服务
|
||||
*/
|
||||
|
||||
class ChatRecordsServer {
|
||||
/**
|
||||
* 分页查询聊天记录
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/chat/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天统计数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getStatistics() {
|
||||
return window.framework.http.get('/chat/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条聊天详情
|
||||
* @param {Number|String} chatId - 聊天记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(chatId) {
|
||||
return window.framework.http.post('/chat/detail', { chatId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天记录
|
||||
* @param {Object} row - 聊天记录数据(包含id或chatId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/chat/delete', {
|
||||
chatId: row.chatId || row.id,
|
||||
id: row.id || row.chatId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('/chat/delete', { chatIds: ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
* @param {Object} data - 消息数据
|
||||
* @param {String} data.sn_code - 设备SN码
|
||||
* @param {String} data.jobId - 职位ID
|
||||
* @param {String} data.content - 消息内容
|
||||
* @param {String} data.chatType - 聊天类型
|
||||
* @returns {Promise}
|
||||
*/
|
||||
sendMessage(data) {
|
||||
return window.framework.http.post('/chat/send', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与某个职位的聊天记录
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {String} params.jobId - 职位ID
|
||||
* @param {String} params.sn_code - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getByJobId(params) {
|
||||
return window.framework.http.get('/chat/by-job', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天记录为CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('/chat/export', param, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
* @param {Object} data - 数据
|
||||
* @param {String} data.chatId - 聊天记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
markAsRead(data) {
|
||||
return window.framework.http.post('/chat/mark-read', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {String} params.sn_code - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getUnreadCount(params) {
|
||||
return window.framework.http.get('/chat/unread-count', params)
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChatRecordsServer()
|
||||
|
||||
90
admin/src/api/operation/task_status_server.js
Normal file
90
admin/src/api/operation/task_status_server.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 任务状态 API 服务
|
||||
*/
|
||||
|
||||
class TaskStatusServer {
|
||||
/**
|
||||
* 分页查询任务状态
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('task/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务统计数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getStatistics() {
|
||||
return window.framework.http.get('/task/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条任务详情
|
||||
* @param {Number|String} taskId - 任务ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(taskId) {
|
||||
return window.framework.http.post('task/detail', { taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
* @param {Object} row - 任务数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.post('/task/update', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
* @param {Object} row - 任务数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('task/delete', { taskId: row.taskId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
* @param {Object} row - 任务数据(包含taskId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
cancel(row) {
|
||||
return window.framework.http.post('task/cancel', { taskId: row.taskId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务
|
||||
* @param {Object} row - 任务数据(包含taskId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
retry(row) {
|
||||
return window.framework.http.post('task/retry', { taskId: row.taskId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出任务列表
|
||||
* @param {Object} param - 导出参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('/task/export', param, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务的指令列表
|
||||
* @param {Number} taskId - 任务ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCommands(taskId) {
|
||||
return window.framework.http.post('/task/commands', { taskId })
|
||||
}
|
||||
}
|
||||
|
||||
export default new TaskStatusServer()
|
||||
|
||||
160
admin/src/api/profile/pla_account_server.js
Normal file
160
admin/src/api/profile/pla_account_server.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 平台账号 API 服务
|
||||
*/
|
||||
|
||||
class PlaAccountServer {
|
||||
|
||||
|
||||
/**
|
||||
* 分页查询平台账号
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/account/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号详情
|
||||
* @param {Number|String} id - 账号ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(id) {
|
||||
return window.framework.http.post('/account/detail', { id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增账号
|
||||
* @param {Object} row - 账号数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(row) {
|
||||
return window.framework.http.post('/account/create', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新账号信息
|
||||
* @param {Object} row - 账号数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.post('/account/update', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除账号
|
||||
* @param {Object} row - 账号数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/account/delete', { id: row.id })
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 导出CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('/account/export', param, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号任务列表
|
||||
* @param {Number|String} accountId - 账号ID
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getTasks(accountId, param) {
|
||||
const { pageOption } = param || {}
|
||||
const queryParams = {
|
||||
id: accountId,
|
||||
page: pageOption?.page || 1,
|
||||
pageSize: pageOption?.pageSize || 10
|
||||
}
|
||||
return window.framework.http.get(`pla_account/tasks`, queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号指令列表
|
||||
* @param {Number|String} accountId - 账号ID
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCommands(accountId, param) {
|
||||
const { pageOption } = param || {}
|
||||
const queryParams = {
|
||||
id: accountId,
|
||||
page: pageOption?.page || 1,
|
||||
pageSize: pageOption?.pageSize || 10
|
||||
}
|
||||
return window.framework.http.get(`pla_account/commands`, queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行指令
|
||||
* @param {Object} param - 指令参数
|
||||
* @param {Number|String} param.id - 账号ID
|
||||
* @param {String} param.commandType - 指令类型
|
||||
* @param {String} param.commandName - 指令名称
|
||||
* @returns {Promise}
|
||||
*/
|
||||
runCommand(param) {
|
||||
return window.framework.http.post(`pla_account/runCommand`, param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指令详情
|
||||
* @param {Number|String} accountId - 账号ID
|
||||
* @param {Number|String} commandId - 指令ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCommandDetail(accountId, commandId) {
|
||||
return window.framework.http.get(`pla_account/commandDetail`, {
|
||||
accountId,
|
||||
commandId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止账号的所有任务
|
||||
* @param {Object} row - 账号数据(包含id和sn_code)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
stopTasks(row) {
|
||||
return window.framework.http.post('/account/stopTasks', {
|
||||
id: row.id,
|
||||
sn_code: row.sn_code
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析地址并更新经纬度
|
||||
* @param {Object} param - 参数对象
|
||||
* @param {Number|String} param.id - 账号ID
|
||||
* @param {String} param.address - 地址(可选)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
parseLocation(param) {
|
||||
return window.framework.http.post('/pla_account/parseLocation', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析地址并更新经纬度
|
||||
* @param {Array<Number|String>} ids - 账号ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchParseLocation(ids) {
|
||||
return window.framework.http.post('/pla_account/batchParseLocation', { ids })
|
||||
}
|
||||
}
|
||||
|
||||
export default new PlaAccountServer()
|
||||
|
||||
45
admin/src/api/profile/resume_info_server.js
Normal file
45
admin/src/api/profile/resume_info_server.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 简历信息 API 服务
|
||||
*/
|
||||
|
||||
class ResumeInfoServer {
|
||||
/**
|
||||
* 分页查询简历信息
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/resume/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简历统计数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getStatistics() {
|
||||
return window.framework.http.get('/resume/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条简历详情
|
||||
* @param {Number|String} id - 简历ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(id) {
|
||||
return window.framework.http.post('/resume/detail', { resumeId: id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除简历
|
||||
* @param {Object} row - 简历数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/resume/delete', { resumeId: row.resumeId || row.id })
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResumeInfoServer()
|
||||
|
||||
65
admin/src/api/statistics/statistics_server.js
Normal file
65
admin/src/api/statistics/statistics_server.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 统计数据 API 服务
|
||||
*/
|
||||
|
||||
class StatisticsServer {
|
||||
/**
|
||||
* 获取按天统计的数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {String} params.deviceSn - 设备SN码
|
||||
* @param {String} params.startDate - 开始日期 (YYYY-MM-DD)
|
||||
* @param {String} params.endDate - 结束日期 (YYYY-MM-DD)
|
||||
* @param {Number} params.days - 统计最近几天(可选,与日期范围二选一)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getDailyStatistics(params) {
|
||||
return window.framework.http.post('/statistics/daily', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实时统计概览
|
||||
* @param {String} deviceSn - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getOverview(deviceSn) {
|
||||
return window.framework.http.get('/statistics/overview', { deviceSn })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取投递数量统计(按天)
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getApplyStatistics(params) {
|
||||
return window.framework.http.post('/statistics/apply', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取找工作数量统计(按天)
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getJobSearchStatistics(params) {
|
||||
return window.framework.http.post('/statistics/job-search', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天/沟通数量统计(按天)
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getChatStatistics(params) {
|
||||
return window.framework.http.post('/statistics/chat', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前正在执行的任务
|
||||
* @param {String} deviceSn - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getRunningTasks(deviceSn) {
|
||||
return window.framework.http.get('/statistics/running-tasks', { deviceSn })
|
||||
}
|
||||
}
|
||||
|
||||
export default new StatisticsServer()
|
||||
80
admin/src/api/system/system_config_server.js
Normal file
80
admin/src/api/system/system_config_server.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 系统配置 API 服务
|
||||
*/
|
||||
|
||||
class SystemConfigServer {
|
||||
/**
|
||||
* 分页查询系统配置
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/config/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置分类列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCategories() {
|
||||
return window.framework.http.get('/config/categories')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条配置详情
|
||||
* @param {String} key - 配置键
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get(key) {
|
||||
return window.framework.http.post('/config/detail', { configKey: key })
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加配置
|
||||
* @param {Object} data - 配置数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(data) {
|
||||
return window.framework.http.post('/config/create', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @param {Object} row - 配置数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.post('/config/update', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新配置
|
||||
* @param {Array} configs - 配置数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchUpdate(configs) {
|
||||
return window.framework.http.post('/config/batch-update', { configs })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
* @param {Object} row - 配置数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/config/delete', { configKey: row.configKey })
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置配置为默认值
|
||||
* @param {String} key - 配置键
|
||||
* @returns {Promise}
|
||||
*/
|
||||
reset(key) {
|
||||
return window.framework.http.post('/config/reset', { configKey: key })
|
||||
}
|
||||
}
|
||||
|
||||
export default new SystemConfigServer()
|
||||
113
admin/src/api/task/task_status_server.js
Normal file
113
admin/src/api/task/task_status_server.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 任务状态 API 服务
|
||||
*/
|
||||
|
||||
class TaskStatusServer {
|
||||
/**
|
||||
* 分页查询任务状态
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('task/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录详情
|
||||
* @param {String} taskId - 任务ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(taskId) {
|
||||
return window.framework.http.post('task/detail', { taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务统计
|
||||
* @returns {Promise}
|
||||
*/
|
||||
statistics() {
|
||||
return window.framework.http.get('task/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务的指令列表
|
||||
* @param {Number|String} taskId - 任务ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCommands(taskId) {
|
||||
return window.framework.http.post('task/commands', { taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增任务
|
||||
* @param {Object} data - 记录数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(data) {
|
||||
return window.framework.http.post('task_status', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
* @param {Object} row - 记录数据(包含taskId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.post('task/update', {
|
||||
taskId: row.taskId || row.id,
|
||||
status: row.status,
|
||||
progress: row.progress,
|
||||
errorMessage: row.errorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
* @param {Object} row - 记录数据(包含taskId)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('task/delete', { taskId: row.taskId || row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('task_status/batch_delete', { ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
* @param {Object} row - 任务数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
cancel(row) {
|
||||
return window.framework.http.post(`task_status/${row.taskId}/cancel`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务
|
||||
* @param {Object} row - 任务数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
retry(row) {
|
||||
return window.framework.http.post(`task_status/${row.taskId}/retry`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('task/export', param, { responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
|
||||
export default new TaskStatusServer()
|
||||
|
||||
81
admin/src/api/work/apply_records_server.js
Normal file
81
admin/src/api/work/apply_records_server.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 投递记录 API 服务
|
||||
*/
|
||||
|
||||
class ApplyRecordsServer {
|
||||
/**
|
||||
* 分页查询投递记录
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('apply/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取投递统计数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getStatistics() {
|
||||
return window.framework.http.get('/apply/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录详情
|
||||
* @param {Number|String} applyId - 记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(applyId) {
|
||||
return window.framework.http.post('apply/detail', { applyId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除投递记录
|
||||
* @param {Object} row - 记录数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/apply/delete', { applyId: row.applyId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('/apply/delete', { applyIds: ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出投递记录为CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('/apply/export', param, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增投递记录
|
||||
* @param {Object} data - 投递记录数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(data) {
|
||||
return window.framework.http.post('/apply/add', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新投递记录
|
||||
* @param {Object} data - 投递记录数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(data) {
|
||||
return window.framework.http.post('/apply/update', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default new ApplyRecordsServer()
|
||||
|
||||
98
admin/src/api/work/job_postings_server.js
Normal file
98
admin/src/api/work/job_postings_server.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 岗位信息 API 服务
|
||||
*/
|
||||
|
||||
class JobPostingsServer {
|
||||
/**
|
||||
* 分页查询岗位信息
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('job/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位统计数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getStatistics() {
|
||||
return window.framework.http.get('/job/statistics')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条岗位详情
|
||||
* @param {Number|String} jobId - 岗位ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(jobId) {
|
||||
return window.framework.http.post('job/detail', { jobId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 职位打招呼(通过MQTT发送指令到boss-automation-nodejs)
|
||||
* @param {Object} data - 打招呼数据
|
||||
* @param {String} data.sn_code - 设备SN码
|
||||
* @param {String} data.encryptJobId - 加密的职位ID
|
||||
* @param {String} data.brandName - 公司名称
|
||||
* @returns {Promise}
|
||||
*/
|
||||
jobGreet(data) {
|
||||
return window.framework.http.post('/job/greet', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {String} params.sn_code - 设备SN码
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getChatList(params) {
|
||||
return window.framework.http.get('/job/chat-list', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
* @param {Object} data - 消息数据
|
||||
* @param {String} data.sn_code - 设备SN码
|
||||
* @param {String} data.jobId - 职位ID
|
||||
* @param {String} data.message - 消息内容
|
||||
* @param {String} data.chatType - 聊天类型
|
||||
* @returns {Promise}
|
||||
*/
|
||||
sendMessage(data) {
|
||||
return window.framework.http.post('/job/send-message', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出岗位信息为CSV
|
||||
* @param {Object} param - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
exportCsv(param) {
|
||||
return window.framework.http.post('/job/export', param, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除岗位信息
|
||||
* @param {Object} row - 岗位数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/job/delete', { jobId: row.jobId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param {Array} ids - ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('/job/delete', { jobIds: ids })
|
||||
}
|
||||
}
|
||||
|
||||
export default new JobPostingsServer()
|
||||
|
||||
63
admin/src/api/work/job_types_server.js
Normal file
63
admin/src/api/work/job_types_server.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 职位类型 API 服务
|
||||
*/
|
||||
|
||||
class JobTypesServer {
|
||||
/**
|
||||
* 分页查询职位类型
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/job_type/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取职位类型详情
|
||||
* @param {Number|String} id - 职位类型ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(id) {
|
||||
return window.framework.http.get('/job_type/detail', { id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增职位类型
|
||||
* @param {Object} row - 职位类型数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(row) {
|
||||
return window.framework.http.post('/job_type/create', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新职位类型信息
|
||||
* @param {Object} row - 职位类型数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.post('/job_type/update', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除职位类型
|
||||
* @param {Object} row - 职位类型数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/job_type/delete', { id: row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的职位类型(用于下拉选择)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAll() {
|
||||
return window.framework.http.get('/job_type/all')
|
||||
}
|
||||
}
|
||||
|
||||
export default new JobTypesServer()
|
||||
|
||||
32
admin/src/framework/admin-framework.js
Normal file
32
admin/src/framework/admin-framework.js
Normal file
File diff suppressed because one or more lines are too long
27
admin/src/main.js
Normal file
27
admin/src/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// 引入依赖
|
||||
import config from "../config/index.js";
|
||||
import componentMap from "./router/component-map.js";
|
||||
import AdminFramework from "./framework/admin-framework.js";
|
||||
import CustomHomePage from "./views/home/index.vue";
|
||||
import deviceModule from "./store/index.js";
|
||||
|
||||
|
||||
const app = AdminFramework.createApp({
|
||||
title: '我的管理系统',
|
||||
apiUrl: config.apiUrl,
|
||||
componentMap: componentMap,
|
||||
HomePage: CustomHomePage, // 可选:自定义首页组件,覆盖整个首页
|
||||
storeModules: {
|
||||
device: deviceModule // 注册设备选择模块
|
||||
}
|
||||
})
|
||||
// 挂载应用
|
||||
app.$mount('#app')
|
||||
|
||||
// 全局暴露 app 实例(方便调试)
|
||||
window.app = app
|
||||
window.rootVue = app
|
||||
// window.framework 已在文件开头暴露,无需重复
|
||||
|
||||
|
||||
|
||||
57
admin/src/router/component-map.js
Normal file
57
admin/src/router/component-map.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// 组件映射表 - 将后端返回的组件路径映射到实际的 Vue 组件
|
||||
// 后端返回的 component 字段需要与这里的 key 对应
|
||||
|
||||
// 导入所有页面组件
|
||||
// 工作管理模块 - 这两个文件在 work/ 和 job/ 两个目录都存在,优先使用 work/
|
||||
import ApplyRecords from '@/views/work/apply_records.vue'
|
||||
import JobPostings from '@/views/work/job_postings.vue'
|
||||
// 账号简历模块
|
||||
import PlaAccount from '@/views/account/pla_account.vue'
|
||||
import PlaAccountDetail from '@/views/account/pla_account_detail.vue'
|
||||
import ResumeInfo from '@/views/account/resume_info.vue'
|
||||
|
||||
|
||||
|
||||
// 任务聊天模块
|
||||
import ChatRecords from '@/views/chat/chat_records.vue'
|
||||
import ChatList from '@/views/chat/chat_list.vue'
|
||||
import TaskStatus from '@/views/task/task_status.vue'
|
||||
|
||||
// 系统设置模块
|
||||
import SystemConfig from '@/views/system/system_config.vue'
|
||||
import JobTypes from '@/views/work/job_types.vue'
|
||||
|
||||
// 首页模块
|
||||
import HomeIndex from '@/views/home/index.vue'
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 组件映射对象
|
||||
* key: 后端返回的组件路径(去掉 .vue 后缀或保留都可以)
|
||||
* value: 实际的 Vue 组件
|
||||
*/
|
||||
const componentMap = {
|
||||
// 工作管理模块
|
||||
'work/apply_records': ApplyRecords,
|
||||
'work/job_postings': JobPostings,
|
||||
|
||||
// 账号简历模块
|
||||
'account/pla_account': PlaAccount,
|
||||
'account/pla_account_detail': PlaAccountDetail,
|
||||
'account/resume_info': ResumeInfo,
|
||||
|
||||
|
||||
// 任务聊天模块
|
||||
'task/chat_records': ChatList, // 使用聊天列表组件
|
||||
'chat/chat_list': ChatList,
|
||||
'task/task_status': TaskStatus,
|
||||
|
||||
// 系统设置模块
|
||||
'system/system_config': SystemConfig,
|
||||
'work/job_types': JobTypes,
|
||||
'home/index': HomeIndex,
|
||||
}
|
||||
|
||||
export default componentMap
|
||||
|
||||
46
admin/src/store/index.js
Normal file
46
admin/src/store/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 设备选择模块 - 用于统计页面的设备切换
|
||||
* 这个模块会被持久化到 localStorage
|
||||
*/
|
||||
const deviceModule = {
|
||||
namespaced: true,
|
||||
|
||||
state: {
|
||||
selectedDeviceSn: '', // 当前选中的设备 SN
|
||||
deviceList: [], // 设备列表
|
||||
},
|
||||
|
||||
mutations: {
|
||||
SET_SELECTED_DEVICE(state, deviceSn) {
|
||||
state.selectedDeviceSn = deviceSn
|
||||
},
|
||||
|
||||
SET_DEVICE_LIST(state, list) {
|
||||
state.deviceList = list
|
||||
// 如果没有选中的设备,且列表不为空,自动选中第一个
|
||||
if (!state.selectedDeviceSn && list.length > 0) {
|
||||
state.selectedDeviceSn = list[0].deviceSn
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setSelectedDevice({ commit }, deviceSn) {
|
||||
commit('SET_SELECTED_DEVICE', deviceSn)
|
||||
},
|
||||
|
||||
setDeviceList({ commit }, list) {
|
||||
commit('SET_DEVICE_LIST', list)
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
selectedDeviceSn: state => state.selectedDeviceSn,
|
||||
deviceList: state => state.deviceList,
|
||||
selectedDevice: state => {
|
||||
return state.deviceList.find(d => d.deviceSn === state.selectedDeviceSn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default deviceModule
|
||||
784
admin/src/views/account/pla_account.vue
Normal file
784
admin/src/views/account/pla_account.vue
Normal file
@@ -0,0 +1,784 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增账号</Button>
|
||||
<Button type="success" @click="batchParseLocation" :loading="batchParseLoading" class="ml10">批量解析位置</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="平台">
|
||||
<Select v-model="gridOption.param.seachOption.platform_type" style="width: 120px" clearable
|
||||
@on-change="query(1)">
|
||||
<Option value="1">Boss直聘</Option>
|
||||
<Option value="2">猎聘</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="在线状态">
|
||||
<Select v-model="gridOption.param.seachOption.is_online" style="width: 120px" clearable
|
||||
@on-change="query(1)">
|
||||
<Option :value="true">在线</Option>
|
||||
<Option :value="false">离线</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules" @on-save="handleSaveSuccess">
|
||||
</editModal>
|
||||
|
||||
<!-- 简历详情弹窗 -->
|
||||
<Modal v-model="resumeModal.visible" :title="resumeModal.title" width="900" :footer-hide="true">
|
||||
<div v-if="resumeModal.loading" style="text-align: center; padding: 40px;">
|
||||
<Spin size="large"></Spin>
|
||||
<p style="margin-top: 20px;">加载简历数据中...</p>
|
||||
</div>
|
||||
<div v-else-if="resumeModal.data" class="resume-detail">
|
||||
<!-- 基本信息 -->
|
||||
<Card title="基本信息" style="margin-bottom: 16px;">
|
||||
<Row :gutter="16">
|
||||
<Col span="8">
|
||||
<p><strong>姓名:</strong>{{ resumeModal.data.fullName || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>性别:</strong>{{ resumeModal.data.gender || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>年龄:</strong>{{ resumeModal.data.age || '-' }}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row :gutter="16" style="margin-top: 12px;">
|
||||
<Col span="8">
|
||||
<p><strong>电话:</strong>{{ resumeModal.data.phone || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>邮箱:</strong>{{ resumeModal.data.email || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>所在地:</strong>{{ resumeModal.data.location || '-' }}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 教育背景 -->
|
||||
<Card title="教育背景" style="margin-bottom: 16px;">
|
||||
<Row :gutter="16">
|
||||
<Col span="8">
|
||||
<p><strong>学历:</strong>{{ resumeModal.data.education || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>专业:</strong>{{ resumeModal.data.major || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>毕业院校:</strong>{{ resumeModal.data.school || '-' }}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 工作信息 -->
|
||||
<Card title="工作信息" style="margin-bottom: 16px;">
|
||||
<Row :gutter="16">
|
||||
<Col span="8">
|
||||
<p><strong>工作年限:</strong>{{ resumeModal.data.workYears || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>当前职位:</strong>{{ resumeModal.data.currentPosition || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>当前公司:</strong>{{ resumeModal.data.currentCompany || '-' }}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 期望信息 -->
|
||||
<Card title="期望信息" style="margin-bottom: 16px;">
|
||||
<Row :gutter="16">
|
||||
<Col span="8">
|
||||
<p><strong>期望职位:</strong>{{ resumeModal.data.expectedPosition || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>期望薪资:</strong>{{ resumeModal.data.expectedSalary || '-' }}</p>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<p><strong>期望地点:</strong>{{ resumeModal.data.expectedLocation || '-' }}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 技能标签 -->
|
||||
<Card title="技能标签" style="margin-bottom: 16px;"
|
||||
v-if="resumeModal.data.skills || resumeModal.data.aiSkillTags">
|
||||
<div v-if="resumeModal.data.aiSkillTags && resumeModal.data.aiSkillTags.length > 0">
|
||||
<Tag v-for="(skill, index) in resumeModal.data.aiSkillTags" :key="index" color="blue"
|
||||
style="margin: 4px;">
|
||||
{{ skill }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div v-else-if="resumeModal.data.skills">
|
||||
<Tag v-for="(skill, index) in parseSkills(resumeModal.data.skills)" :key="index" color="blue"
|
||||
style="margin: 4px;">
|
||||
{{ skill }}
|
||||
</Tag>
|
||||
</div>
|
||||
<p v-else style="color: #999;">暂无技能标签</p>
|
||||
</Card>
|
||||
|
||||
<!-- AI分析 -->
|
||||
<Card title="AI分析" style="margin-bottom: 16px;" v-if="resumeModal.data.aiCompetitiveness">
|
||||
<Row :gutter="16">
|
||||
<Col span="24">
|
||||
<p><strong>竞争力评分:</strong>
|
||||
<Tag :color="getScoreColor(resumeModal.data.aiCompetitiveness)" size="large">
|
||||
{{ resumeModal.data.aiCompetitiveness }} 分
|
||||
</Tag>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row :gutter="16" style="margin-top: 12px;">
|
||||
<Col span="24">
|
||||
<p><strong>优势分析:</strong></p>
|
||||
<p style="color: #19be6b; padding: 8px; background: #f0f9ff; border-radius: 4px;">
|
||||
{{ resumeModal.data.aiStrengths || '-' }}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row :gutter="16" style="margin-top: 12px;">
|
||||
<Col span="24">
|
||||
<p><strong>劣势分析:</strong></p>
|
||||
<p style="color: #ed4014; padding: 8px; background: #fff1f0; border-radius: 4px;">
|
||||
{{ resumeModal.data.aiWeaknesses || '-' }}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row :gutter="16" style="margin-top: 12px;">
|
||||
<Col span="24">
|
||||
<p><strong>职业建议:</strong></p>
|
||||
<p style="color: #2d8cf0; padding: 8px; background: #f0faff; border-radius: 4px;">
|
||||
{{ resumeModal.data.aiCareerSuggestion || '-' }}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 项目经验 -->
|
||||
<Card title="项目经验" style="margin-bottom: 16px;" v-if="resumeModal.data.projectExperience">
|
||||
<Timeline v-if="parseProjectExp(resumeModal.data.projectExperience).length > 0">
|
||||
<TimelineItem v-for="(project, index) in parseProjectExp(resumeModal.data.projectExperience)"
|
||||
:key="index">
|
||||
<p><strong>{{ project.name || project.projectName }}</strong></p>
|
||||
<p style="color: #999; font-size: 12px;">{{ project.roleName || project.role }}</p>
|
||||
<p style="margin-top: 8px;">{{ project.projectDesc || project.description }}</p>
|
||||
<p v-if="project.performance" style="margin-top: 4px; color: #19be6b;">
|
||||
<Icon type="ios-trophy" /> {{ project.performance }}
|
||||
</p>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
<p v-else style="color: #999;">暂无项目经验</p>
|
||||
</Card>
|
||||
|
||||
<!-- 工作经历 -->
|
||||
<Card title="工作经历" v-if="resumeModal.data.workExperience">
|
||||
<Timeline v-if="parseWorkExp(resumeModal.data.workExperience).length > 0">
|
||||
<TimelineItem v-for="(work, index) in parseWorkExp(resumeModal.data.workExperience)"
|
||||
:key="index">
|
||||
<p><strong>{{ work.companyName }}</strong> - {{ work.positionName }}</p>
|
||||
<p style="color: #999; font-size: 12px;">
|
||||
{{ work.startDate }} ~ {{ work.endDate }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">{{ work.workContent || work.description }}</p>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
<p v-else style="color: #999;">暂无工作经历</p>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-else style="text-align: center; padding: 40px; color: #999;">
|
||||
<Icon type="ios-document-outline" size="60" />
|
||||
<p style="margin-top: 20px;">暂无简历数据</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import plaAccountServer from '@/api/profile/pla_account_server.js'
|
||||
import jobTypesServer from '@/api/work/job_types_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["name"] = [{ required: true, message: '请填写账户名', trigger: 'blur' }]
|
||||
rules["sn_code"] = [{ required: true, message: '请填写设备SN码', trigger: 'blur' }]
|
||||
rules["platform_type"] = [{ required: true, message: '请选择平台', trigger: 'change' }]
|
||||
rules["login_name"] = [{ required: true, message: '请填写登录名', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
serverInstance: plaAccountServer,
|
||||
jobTypeOptions: [],
|
||||
batchParseLoading: false,
|
||||
seachTypes: [
|
||||
{ key: 'name', value: '账户名' },
|
||||
{ key: 'login_name', value: '登录名' },
|
||||
{ key: 'sn_code', value: '设备SN码' }
|
||||
],
|
||||
resumeModal: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
title: '在线简历详情',
|
||||
data: null
|
||||
},
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'name',
|
||||
value: '',
|
||||
platform_type: null,
|
||||
is_online: null,
|
||||
is_online: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '账户名', key: 'name', minWidth: 150 },
|
||||
{ title: '设备SN码', key: 'sn_code', minWidth: 120 },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform_type',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const platformMap = {
|
||||
'1': { text: 'Boss直聘', color: 'blue' },
|
||||
'2': { text: '猎聘', color: 'green' }
|
||||
}
|
||||
const platform = platformMap[params.row.platform_type] || { text: params.row.platform_type, color: 'default' }
|
||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||
}
|
||||
},
|
||||
{ title: '登录名', key: 'login_name', minWidth: 150 },
|
||||
{ title: '搜索关键词', key: 'keyword', minWidth: 150 },
|
||||
{ title: '用户地址', key: 'user_address', minWidth: 150 },
|
||||
{
|
||||
title: '经纬度',
|
||||
key: 'location',
|
||||
minWidth: 150,
|
||||
render: (h, params) => {
|
||||
const lon = params.row.user_longitude;
|
||||
const lat = params.row.user_latitude;
|
||||
if (lon && lat) {
|
||||
return h('span', `${lat}, ${lon}`)
|
||||
}
|
||||
return h('span', { style: { color: '#999' } }, '未设置')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '在线状态',
|
||||
key: 'is_online',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.is_online ? 'success' : 'default' }
|
||||
}, params.row.is_online ? '在线' : '离线')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '自动投递',
|
||||
key: 'auto_deliver',
|
||||
com: "Radio",
|
||||
minWidth: 100,
|
||||
options: [
|
||||
{ value: 1, label: '开启' },
|
||||
{ value: 0, label: '关闭' }
|
||||
],
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.auto_deliver ? 'success' : 'default' }
|
||||
}, params.row.auto_deliver ? '开启' : '关闭')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '自动沟通',
|
||||
key: 'auto_chat',
|
||||
"com": "Radio",
|
||||
options: [
|
||||
{ value: 1, label: '开启' },
|
||||
{ value: 0, label: '关闭' }
|
||||
],
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.auto_chat ? 'success' : 'default' }
|
||||
}, params.row.auto_chat ? '开启' : '关闭')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '自动活跃',
|
||||
key: 'auto_active',
|
||||
"com": "Radio",
|
||||
options: [
|
||||
{ value: 1, label: '开启' },
|
||||
{ value: 0, label: '关闭' }
|
||||
],
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.auto_active ? 'success' : 'default' }
|
||||
}, params.row.auto_active ? '开启' : '关闭')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
key: 'is_enabled',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('i-switch', {
|
||||
props: {
|
||||
value: Boolean(params.row.is_enabled),
|
||||
size: 'large'
|
||||
},
|
||||
on: {
|
||||
'on-change': (value) => {
|
||||
this.toggleEnabled(params.row, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{ title: '创建时间', key: 'create_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 450,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '详情',
|
||||
type: 'info',
|
||||
click: () => {
|
||||
this.showAccountDetails(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '查看简历',
|
||||
type: 'success',
|
||||
click: () => {
|
||||
this.showResume(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '解析位置',
|
||||
type: 'success',
|
||||
click: () => {
|
||||
this.parseLocation(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '停止任务',
|
||||
type: 'warning',
|
||||
click: () => {
|
||||
this.stopTasks(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '账户名', key: 'name', type: 'text', required: true },
|
||||
{ title: '设备SN码', key: 'sn_code', type: 'text', required: true },
|
||||
{
|
||||
title: '平台', key: 'platform_type', type: 'select', required: true, options: [
|
||||
{ value: '1', label: 'Boss直聘' },
|
||||
{ value: '2', label: '猎聘' }
|
||||
]
|
||||
},
|
||||
{ title: '登录名', key: 'login_name', type: 'text', required: true },
|
||||
{ title: '密码', key: 'pwd', type: 'password' },
|
||||
{ title: '搜索关键词', key: 'keyword', type: 'text' },
|
||||
{ title: '启用状态', key: 'is_enabled', type: 'switch' },
|
||||
{ title: '在线状态', key: 'is_online', type: 'switch' },
|
||||
{
|
||||
title: '职位类型',
|
||||
key: 'job_type_id',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: this.jobTypeOptions || []
|
||||
},
|
||||
{ title: '用户地址', key: 'user_address', type: 'text', placeholder: '请输入用户地址,如:北京市朝阳区' },
|
||||
// 自动投递配置
|
||||
{
|
||||
title: '自动投递', key: 'auto_deliver', "com": "Radio", options: [
|
||||
{ value: 1, label: '开启' },
|
||||
{ value: 0, label: '关闭' }
|
||||
],
|
||||
},
|
||||
{ title: '最低薪资(元)', key: 'min_salary', type: 'number', placeholder: '最低薪资,0表示不限制' },
|
||||
{ title: '最高薪资(元)', key: 'max_salary', type: 'number', placeholder: '最高薪资,0表示不限制' },
|
||||
// 自动沟通配置
|
||||
{
|
||||
title: '自动沟通', key: 'auto_chat', "com": "Radio", options: [
|
||||
{ value: 1, label: '开启' },
|
||||
{ value: 0, label: '关闭' }
|
||||
],
|
||||
},
|
||||
{ title: '沟通间隔(分钟)', key: 'chat_interval', type: 'number', placeholder: '沟通间隔时间,默认30分钟' },
|
||||
{ title: '自动回复', key: 'auto_reply', type: 'switch' },
|
||||
// 自动活跃配置
|
||||
{
|
||||
title: '自动活跃', key: 'auto_active', "com": "Radio", options: [
|
||||
{ value: 1, label: '开启' },
|
||||
{ value: 0, label: '关闭' }
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
this.loadJobTypes()
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
plaAccountServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
// 将布尔字段从 0/1 转换为 true/false,以便开关组件正确显示
|
||||
const editData = { ...row }
|
||||
const booleanFields = ['is_enabled', 'is_online', 'auto_deliver', 'auto_chat', 'auto_reply', 'auto_active']
|
||||
booleanFields.forEach(field => {
|
||||
if (editData[field] !== undefined && editData[field] !== null) {
|
||||
editData[field] = Boolean(editData[field])
|
||||
}
|
||||
})
|
||||
this.$refs.editModal.showModal(editData)
|
||||
},
|
||||
toggleEnabled(row, value) {
|
||||
const action = value ? '启用' : '禁用'
|
||||
this.$Modal.confirm({
|
||||
title: `确认${action}`,
|
||||
content: `确定要${action}账号 "${row.name}" 吗?${!value ? '禁用后该账号将不会执行自动任务。' : ''}`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await plaAccountServer.update({ ...row, is_enabled: value ? 1 : 0 })
|
||||
this.$Message.success(`${action}成功!`)
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
this.$Message.error(`${action}失败`)
|
||||
// 恢复开关状态
|
||||
row.is_enabled = value ? 0 : 1
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
// 取消时恢复开关状态
|
||||
row.is_enabled = value ? 0 : 1
|
||||
}
|
||||
})
|
||||
},
|
||||
loadJobTypes() {
|
||||
jobTypesServer.getAll().then(res => {
|
||||
if (res.code === 0 && res.data) {
|
||||
this.jobTypeOptions = res.data.map(item => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
// 更新 editColumns 中的选项
|
||||
const jobTypeColumn = this.gridOption.editColumns.find(col => col.key === 'job_type_id')
|
||||
if (jobTypeColumn) {
|
||||
jobTypeColumn.options = this.jobTypeOptions
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('加载职位类型失败:', err)
|
||||
})
|
||||
},
|
||||
stopTasks(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认停止',
|
||||
content: `确定要停止账号 "${row.name}" 的所有任务吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await plaAccountServer.stopTasks(row)
|
||||
this.$Message.success('停止任务成功!')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
this.$Message.error('停止任务失败:' + (error.message || '请稍后重试'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await plaAccountServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
plaAccountServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '平台账号.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'name',
|
||||
value: '',
|
||||
platform_type: null,
|
||||
is_online: null
|
||||
}
|
||||
this.query(1)
|
||||
},
|
||||
async handleSaveSuccess({ data }) {
|
||||
try {
|
||||
// 将布尔字段从 true/false 转换为 1/0
|
||||
const saveData = { ...data }
|
||||
const booleanFields = ['is_enabled', 'is_online', 'auto_deliver', 'auto_chat', 'auto_reply', 'auto_active']
|
||||
booleanFields.forEach(field => {
|
||||
if (saveData[field] !== undefined && saveData[field] !== null) {
|
||||
saveData[field] = saveData[field] ? 1 : 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 根据是否有 id 判断是新增还是更新
|
||||
if (saveData.id) {
|
||||
await plaAccountServer.update(saveData)
|
||||
} else {
|
||||
await plaAccountServer.add(saveData)
|
||||
}
|
||||
|
||||
this.$Message.success('保存成功!')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
this.$Message.error('保存失败:' + (error.message || '请稍后重试'))
|
||||
}
|
||||
},
|
||||
// 显示账号详情
|
||||
showAccountDetails(row) {
|
||||
this.$router.push({
|
||||
path: '/pla_account/pla_account_detail',
|
||||
query: { id: row.id }
|
||||
})
|
||||
},
|
||||
// 查看简历
|
||||
async showResume(row) {
|
||||
this.resumeModal.visible = true
|
||||
this.resumeModal.loading = true
|
||||
this.resumeModal.data = null
|
||||
this.resumeModal.title = `${row.name} - 在线简历`
|
||||
|
||||
try {
|
||||
// 根据 sn_code 和 platform 获取简历
|
||||
const platformMap = {
|
||||
'1': 'boss',
|
||||
'2': 'liepin'
|
||||
}
|
||||
const platform = platformMap[row.platform_type] || 'boss'
|
||||
|
||||
// admin 会自动加 /admin_api 前缀
|
||||
const res = await window.framework.http.get(`/resume/get-by-device?sn_code=${row.sn_code}&platform=${platform}`)
|
||||
|
||||
if (res.code === 0) {
|
||||
this.resumeModal.data = res.data
|
||||
} else {
|
||||
this.$Message.warning(res.message || '未找到简历数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取简历失败:', error)
|
||||
this.$Message.error('获取简历失败:' + (error.message || '请稍后重试'))
|
||||
} finally {
|
||||
this.resumeModal.loading = false
|
||||
}
|
||||
},
|
||||
// 解析技能标签
|
||||
parseSkills(skills) {
|
||||
if (!skills) return []
|
||||
if (Array.isArray(skills)) return skills
|
||||
try {
|
||||
return JSON.parse(skills)
|
||||
} catch (e) {
|
||||
return skills.split(',').map(s => s.trim()).filter(s => s)
|
||||
}
|
||||
},
|
||||
// 解析项目经验
|
||||
parseProjectExp(projectExp) {
|
||||
if (!projectExp) return []
|
||||
if (Array.isArray(projectExp)) return projectExp
|
||||
try {
|
||||
return JSON.parse(projectExp)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
// 解析工作经历
|
||||
parseWorkExp(workExp) {
|
||||
if (!workExp) return []
|
||||
if (Array.isArray(workExp)) return workExp
|
||||
try {
|
||||
return JSON.parse(workExp)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
// 获取评分颜色
|
||||
getScoreColor(score) {
|
||||
if (score >= 80) return 'success'
|
||||
if (score >= 60) return 'warning'
|
||||
return 'error'
|
||||
},
|
||||
// 解析单个账号的位置
|
||||
async parseLocation(row) {
|
||||
if (!row.user_address || row.user_address.trim() === '') {
|
||||
this.$Message.warning('请先设置用户地址')
|
||||
return
|
||||
}
|
||||
|
||||
this.$Modal.confirm({
|
||||
title: '确认解析位置',
|
||||
content: `确定要解析账号 "${row.name}" 的地址 "${row.user_address}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await window.framework.http.post('/pla_account/parseLocation', {
|
||||
id: row.id,
|
||||
address: row.user_address
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
this.$Message.success('位置解析成功!')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || '位置解析失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('位置解析失败:', error)
|
||||
this.$Message.error('位置解析失败:' + (error.message || '请稍后重试'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 批量解析位置
|
||||
async batchParseLocation() {
|
||||
const selectedRows = this.gridOption.data.filter(row => row.user_address && row.user_address.trim() !== '')
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
this.$Message.warning('当前页面没有已设置地址的账号')
|
||||
return
|
||||
}
|
||||
|
||||
this.$Modal.confirm({
|
||||
title: '确认批量解析位置',
|
||||
content: `确定要批量解析 ${selectedRows.length} 个账号的位置吗?`,
|
||||
onOk: async () => {
|
||||
this.batchParseLoading = true
|
||||
try {
|
||||
const ids = selectedRows.map(row => row.id)
|
||||
const res = await window.framework.http.post('/pla_account/batchParseLocation', { ids })
|
||||
|
||||
if (res.code === 0) {
|
||||
const result = res.data
|
||||
const successCount = result.success || 0
|
||||
const failedCount = result.failed || 0
|
||||
|
||||
let message = `批量解析完成:成功 ${successCount} 个`
|
||||
if (failedCount > 0) {
|
||||
message += `,失败 ${failedCount} 个`
|
||||
}
|
||||
|
||||
this.$Message.success(message)
|
||||
|
||||
// 如果有失败的,显示详细信息
|
||||
if (failedCount > 0 && result.details) {
|
||||
const failedDetails = result.details.filter(d => !d.success)
|
||||
console.log('解析失败的账号:', failedDetails)
|
||||
}
|
||||
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || '批量解析失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量位置解析失败:', error)
|
||||
this.$Message.error('批量位置解析失败:' + (error.message || '请稍后重试'))
|
||||
} finally {
|
||||
this.batchParseLoading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.resume-detail {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.resume-detail p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.resume-detail strong {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
1419
admin/src/views/account/pla_account_detail.vue
Normal file
1419
admin/src/views/account/pla_account_detail.vue
Normal file
File diff suppressed because it is too large
Load Diff
192
admin/src/views/account/resume_info.vue
Normal file
192
admin/src/views/account/resume_info.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增简历</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="平台">
|
||||
<Select v-model="gridOption.param.seachOption.platform" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="boss">Boss直聘</Option>
|
||||
<Option value="liepin">猎聘</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import resumeInfoServer from '@/api/profile/resume_info_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["sn_code"] = [{ required: true, message: '请填写设备SN码', trigger: 'blur' }]
|
||||
rules["platform"] = [{ required: true, message: '请选择平台', trigger: 'change' }]
|
||||
rules["name"] = [{ required: true, message: '请填写姓名', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'name', value: '姓名' },
|
||||
{ key: 'sn_code', value: '设备SN码' },
|
||||
{ key: 'phone', value: '电话' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'name',
|
||||
value: '',
|
||||
platform: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '设备SN码', key: 'sn_code', minWidth: 120 },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const platformMap = {
|
||||
'boss': { text: 'Boss直聘', color: 'blue' },
|
||||
'liepin': { text: '猎聘', color: 'green' }
|
||||
}
|
||||
const platform = platformMap[params.row.platform] || { text: params.row.platform, color: 'default' }
|
||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||
}
|
||||
},
|
||||
{ title: '姓名', key: 'name', minWidth: 120 },
|
||||
{ title: '电话', key: 'phone', minWidth: 130 },
|
||||
{ title: '邮箱', key: 'email', minWidth: 180 },
|
||||
{ title: '期望职位', key: 'expectedPosition', minWidth: 150 },
|
||||
{ title: '期望薪资', key: 'expectedSalary', minWidth: 120 },
|
||||
{ title: '工作经验', key: 'workExperience', minWidth: 100 },
|
||||
{ title: '学历', key: 'education', minWidth: 100 },
|
||||
{ title: '更新时间', key: 'last_modify_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '设备SN码', key: 'sn_code', type: 'text', required: true },
|
||||
{ title: '平台', key: 'platform', type: 'select', required: true, options: [
|
||||
{ value: 'boss', label: 'Boss直聘' },
|
||||
{ value: 'liepin', label: '猎聘' }
|
||||
]},
|
||||
{ title: '姓名', key: 'name', type: 'text', required: true },
|
||||
{ title: '电话', key: 'phone', type: 'text' },
|
||||
{ title: '邮箱', key: 'email', type: 'text' },
|
||||
{ title: '期望职位', key: 'expectedPosition', type: 'text' },
|
||||
{ title: '期望薪资', key: 'expectedSalary', type: 'text' },
|
||||
{ title: '工作年限', key: 'workExperience', type: 'text' },
|
||||
{ title: '学历', key: 'education', type: 'text' },
|
||||
{ title: '技能', key: 'skills', type: 'textarea' },
|
||||
{ title: '自我介绍', key: 'selfIntroduction', type: 'textarea' },
|
||||
{ title: '简历内容', key: 'resumeContent', type: 'textarea' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
resumeInfoServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await resumeInfoServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
resumeInfoServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '简历信息.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'name',
|
||||
value: '',
|
||||
platform: null
|
||||
}
|
||||
this.query(1)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
169
admin/src/views/ai/ai_messages.vue
Normal file
169
admin/src/views/ai/ai_messages.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="消息类型">
|
||||
<Select v-model="gridOption.param.seachOption.message_type" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="question">提问</Option>
|
||||
<Option value="feedback">反馈</Option>
|
||||
<Option value="suggestion">建议</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="处理状态">
|
||||
<Select v-model="gridOption.param.seachOption.status" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="pending">待处理</Option>
|
||||
<Option value="processing">处理中</Option>
|
||||
<Option value="completed">已完成</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"> </editModal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
|
||||
import ai_messagesServer from '@/api/ai/ai_messages_server.js'
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
|
||||
rules["user_id"] = [{ required: true, type: "number", message: '请填写用户ID', trigger: 'change' }];
|
||||
rules["message_type"] = [{ required: true, message: '请填写消息类型' }];
|
||||
rules["content"] = [{ required: true, message: '请填写消息内容' }];
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'user_id', value: '用户ID' },
|
||||
{ key: 'nickname', value: '用户昵称' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'nickname',
|
||||
value: '',
|
||||
message_type: null,
|
||||
status: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '用户ID', key: 'user_id', minWidth: 120 },
|
||||
{ title: '用户昵称', key: 'nickname', minWidth: 120 },
|
||||
{ title: '消息类型', key: 'message_type', minWidth: 120 },
|
||||
{ title: '消息内容', key: 'content', minWidth: 300 },
|
||||
{ title: 'AI回复', key: 'ai_response', minWidth: 300 },
|
||||
{ title: '创建时间', key: 'create_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '用户ID', key: 'user_id', type: 'number', required: true },
|
||||
{ title: '消息类型', key: 'message_type', type: 'text', required: true },
|
||||
{ title: '消息内容', key: 'content', type: 'textarea', required: true },
|
||||
{ title: 'AI回复', key: 'ai_response', type: 'textarea' },
|
||||
{ title: '处理状态', key: 'status', type: 'select', options: [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'processing', label: '处理中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'failed', label: '处理失败' }
|
||||
]}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1);
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page;
|
||||
ai_messagesServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows;
|
||||
this.gridOption.param.pageOption.total = res.data.count;
|
||||
});
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal();
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row);
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await ai_messagesServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
ai_messagesServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, 'AI消息管理.csv');
|
||||
});
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'nickname',
|
||||
value: '',
|
||||
message_type: null,
|
||||
status: null
|
||||
};
|
||||
this.query(1);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key);
|
||||
return selected ? selected.value : '请选择搜索类型';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
653
admin/src/views/chat/chat_list.vue
Normal file
653
admin/src/views/chat/chat_list.vue
Normal file
@@ -0,0 +1,653 @@
|
||||
<template>
|
||||
<div class="chat-list-container">
|
||||
<!-- 左侧会话列表 -->
|
||||
<div class="conversation-list">
|
||||
<div class="conversation-header">
|
||||
<h3>聊天列表</h3>
|
||||
<div class="header-actions">
|
||||
<Select v-model="conversationFilter.platform" style="width: 120px" placeholder="平台" clearable @on-change="loadConversations">
|
||||
<Option value="boss">Boss直聘</Option>
|
||||
<Option value="liepin">猎聘</Option>
|
||||
</Select>
|
||||
<Input
|
||||
v-model="conversationFilter.search"
|
||||
placeholder="搜索公司/职位"
|
||||
search
|
||||
style="width: 200px; margin-left: 10px"
|
||||
@on-search="loadConversations"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversation-items">
|
||||
<div
|
||||
v-for="conversation in conversations"
|
||||
:key="conversation.conversationId"
|
||||
:class="['conversation-item', { active: activeConversation?.conversationId === conversation.conversationId }]"
|
||||
@click="selectConversation(conversation)"
|
||||
>
|
||||
<div class="conversation-avatar">
|
||||
<Avatar :style="{ background: conversation.platform === 'boss' ? '#00b38a' : '#00a6ff' }">
|
||||
{{ conversation.companyName ? conversation.companyName.substring(0, 1) : 'C' }}
|
||||
</Avatar>
|
||||
<Badge v-if="conversation.unreadCount > 0" :count="conversation.unreadCount" />
|
||||
</div>
|
||||
<div class="conversation-info">
|
||||
<div class="conversation-title">
|
||||
<strong>{{ conversation.companyName || '未知公司' }}</strong>
|
||||
<Tag :color="conversation.platform === 'boss' ? 'success' : 'primary'" size="small">
|
||||
{{ conversation.platform === 'boss' ? 'Boss' : '猎聘' }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="conversation-subtitle">{{ conversation.jobTitle || '未知职位' }}</div>
|
||||
<div class="conversation-last-message">
|
||||
{{ conversation.lastMessage || '暂无消息' }}
|
||||
</div>
|
||||
<div class="conversation-time">{{ formatTime(conversation.lastMessageTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="conversations.length === 0" class="empty-state">
|
||||
<p>暂无聊天记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧聊天窗口 -->
|
||||
<div class="chat-window">
|
||||
<div v-if="activeConversation" class="chat-content">
|
||||
<!-- 聊天头部 -->
|
||||
<div class="chat-header">
|
||||
<div class="chat-header-info">
|
||||
<h3>{{ activeConversation.companyName || '未知公司' }}</h3>
|
||||
<p>{{ activeConversation.jobTitle || '未知职位' }} - {{ activeConversation.hrName || 'HR' }}</p>
|
||||
</div>
|
||||
<div class="chat-header-actions">
|
||||
<Button icon="md-refresh" @click="loadChatMessages">刷新</Button>
|
||||
<Button icon="md-information-circle" @click="showJobDetail">职位详情</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<div class="chat-messages" ref="chatMessages">
|
||||
<div
|
||||
v-for="message in chatMessages"
|
||||
:key="message.id"
|
||||
:class="['message-item', message.direction === 'sent' ? 'message-sent' : 'message-received']"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<Avatar v-if="message.direction === 'received'" icon="ios-person" />
|
||||
<Avatar v-else icon="ios-chatbubbles" />
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-meta">
|
||||
<span class="message-sender">
|
||||
{{ message.direction === 'sent' ? '我' : (message.hrName || 'HR') }}
|
||||
</span>
|
||||
<span class="message-time">{{ formatTime(message.sendTime || message.receiveTime) }}</span>
|
||||
<Tag v-if="message.isAiGenerated" color="purple" size="small">AI生成</Tag>
|
||||
</div>
|
||||
<div class="message-bubble">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
<div v-if="message.isInterviewInvitation" class="interview-invitation">
|
||||
<Icon type="md-calendar" />
|
||||
<span>面试邀约: {{ message.interviewType === 'online' ? '线上' : '线下' }}</span>
|
||||
<span v-if="message.interviewTime">{{ formatTime(message.interviewTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="chatMessages.length === 0" class="empty-messages">
|
||||
<p>暂无聊天消息</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息输入框 -->
|
||||
<div class="chat-input">
|
||||
<Input
|
||||
v-model="messageInput"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入消息内容..."
|
||||
@on-enter="handleSendMessage"
|
||||
/>
|
||||
<div class="chat-input-actions">
|
||||
<Button type="primary" icon="md-send" @click="handleSendMessage" :loading="sending">发送</Button>
|
||||
<Button icon="md-bulb" @click="generateAiMessage">AI生成</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未选择会话的占位 -->
|
||||
<div v-else class="chat-placeholder">
|
||||
<Icon type="ios-chatbubbles" size="64" color="#dcdee2" />
|
||||
<p>请选择一个会话开始聊天</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import chatRecordsServer from '@/api/operation/chat_records_server.js'
|
||||
|
||||
export default {
|
||||
name: 'ChatList',
|
||||
data() {
|
||||
return {
|
||||
// 会话列表
|
||||
conversations: [],
|
||||
activeConversation: null,
|
||||
conversationFilter: {
|
||||
platform: null,
|
||||
search: ''
|
||||
},
|
||||
|
||||
// 聊天消息
|
||||
chatMessages: [],
|
||||
messageInput: '',
|
||||
sending: false,
|
||||
|
||||
// 定时刷新
|
||||
refreshTimer: null,
|
||||
refreshInterval: 10000, // 10秒刷新一次
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadConversations()
|
||||
this.startAutoRefresh()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopAutoRefresh()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载会话列表
|
||||
*/
|
||||
async loadConversations() {
|
||||
try {
|
||||
const res = await chatRecordsServer.page({
|
||||
seachOption: {
|
||||
platform: this.conversationFilter.platform,
|
||||
key: 'companyName',
|
||||
value: this.conversationFilter.search
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
}
|
||||
})
|
||||
|
||||
// 按 conversationId 分组聊天记录
|
||||
const conversationMap = new Map()
|
||||
const rows = res.data?.rows || res.data?.list || []
|
||||
|
||||
rows.forEach(record => {
|
||||
const convId = record.conversationId || `${record.jobId}_${record.sn_code}`
|
||||
|
||||
if (!conversationMap.has(convId)) {
|
||||
conversationMap.set(convId, {
|
||||
conversationId: convId,
|
||||
jobId: record.jobId,
|
||||
sn_code: record.sn_code,
|
||||
platform: record.platform,
|
||||
companyName: record.companyName,
|
||||
jobTitle: record.jobTitle,
|
||||
hrName: record.hrName,
|
||||
lastMessage: record.content,
|
||||
lastMessageTime: record.sendTime || record.receiveTime || new Date(),
|
||||
unreadCount: 0,
|
||||
messages: []
|
||||
})
|
||||
}
|
||||
|
||||
const conv = conversationMap.get(convId)
|
||||
conv.messages.push(record)
|
||||
|
||||
// 更新最后一条消息
|
||||
const lastTime = new Date(conv.lastMessageTime).getTime()
|
||||
const currentTime = new Date(record.sendTime || record.receiveTime || new Date()).getTime()
|
||||
if (currentTime > lastTime) {
|
||||
conv.lastMessage = record.content
|
||||
conv.lastMessageTime = record.sendTime || record.receiveTime
|
||||
}
|
||||
})
|
||||
|
||||
this.conversations = Array.from(conversationMap.values())
|
||||
.sort((a, b) => new Date(b.lastMessageTime) - new Date(a.lastMessageTime))
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载会话列表失败:', error)
|
||||
this.$Message.error('加载会话列表失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择会话
|
||||
*/
|
||||
selectConversation(conversation) {
|
||||
this.activeConversation = conversation
|
||||
this.loadChatMessages()
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载聊天消息
|
||||
*/
|
||||
async loadChatMessages() {
|
||||
if (!this.activeConversation) return
|
||||
|
||||
try {
|
||||
const res = await chatRecordsServer.getByJobId({
|
||||
jobId: this.activeConversation.jobId,
|
||||
sn_code: this.activeConversation.sn_code
|
||||
})
|
||||
|
||||
this.chatMessages = res.data.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.receiveTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.receiveTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom()
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
this.$Message.error('加载聊天消息失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async handleSendMessage() {
|
||||
if (!this.messageInput.trim()) {
|
||||
this.$Message.warning('请输入消息内容')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.activeConversation) {
|
||||
this.$Message.warning('请先选择一个会话')
|
||||
return
|
||||
}
|
||||
|
||||
this.sending = true
|
||||
|
||||
try {
|
||||
await chatRecordsServer.sendMessage({
|
||||
sn_code: this.activeConversation.sn_code,
|
||||
jobId: this.activeConversation.jobId,
|
||||
content: this.messageInput,
|
||||
chatType: 'reply',
|
||||
platform: this.activeConversation.platform
|
||||
})
|
||||
|
||||
this.$Message.success('消息发送成功')
|
||||
this.messageInput = ''
|
||||
|
||||
// 重新加载消息
|
||||
await this.loadChatMessages()
|
||||
|
||||
} catch (error) {
|
||||
this.$Message.error('消息发送失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
this.sending = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* AI生成消息
|
||||
*/
|
||||
generateAiMessage() {
|
||||
this.$Message.info('AI消息生成功能开发中...')
|
||||
// TODO: 调用AI接口生成消息
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示职位详情
|
||||
*/
|
||||
showJobDetail() {
|
||||
if (!this.activeConversation) return
|
||||
|
||||
this.$Modal.info({
|
||||
title: '职位详情',
|
||||
width: 600,
|
||||
render: (h) => {
|
||||
return h('div', [
|
||||
h('p', [h('strong', '公司名称: '), this.activeConversation.companyName]),
|
||||
h('p', [h('strong', '职位名称: '), this.activeConversation.jobTitle]),
|
||||
h('p', [h('strong', 'HR: '), this.activeConversation.hrName || '未知']),
|
||||
h('p', [h('strong', '平台: '), this.activeConversation.platform === 'boss' ? 'Boss直聘' : '猎聘']),
|
||||
h('p', [h('strong', '职位ID: '), this.activeConversation.jobId])
|
||||
])
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
formatTime(time) {
|
||||
if (!time) return ''
|
||||
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// 1分钟内
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
// 1小时内
|
||||
if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
}
|
||||
|
||||
// 今天
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return date.toTimeString().substring(0, 5)
|
||||
}
|
||||
|
||||
// 昨天
|
||||
const yesterday = new Date(now)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
if (date.toDateString() === yesterday.toDateString()) {
|
||||
return '昨天 ' + date.toTimeString().substring(0, 5)
|
||||
}
|
||||
|
||||
// 其他
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${date.toTimeString().substring(0, 5)}`
|
||||
},
|
||||
|
||||
/**
|
||||
* 滚动到底部
|
||||
*/
|
||||
scrollToBottom() {
|
||||
const messagesEl = this.$refs.chatMessages
|
||||
if (messagesEl) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 开始自动刷新
|
||||
*/
|
||||
startAutoRefresh() {
|
||||
// 启动定时器,每10秒刷新一次
|
||||
this.refreshTimer = setInterval(() => {
|
||||
// 如果有选中的会话,刷新消息
|
||||
if (this.activeConversation) {
|
||||
this.loadChatMessages()
|
||||
}
|
||||
// 刷新会话列表
|
||||
this.loadConversations()
|
||||
}, this.refreshInterval)
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止自动刷新
|
||||
*/
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer)
|
||||
this.refreshTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-list-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 120px);
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左侧会话列表 */
|
||||
.conversation-list {
|
||||
width: 320px;
|
||||
border-right: 1px solid #e8eaec;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.conversation-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
}
|
||||
|
||||
.conversation-header h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.conversation-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.conversation-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.conversation-item.active {
|
||||
background-color: #e8f4ff;
|
||||
}
|
||||
|
||||
.conversation-avatar {
|
||||
position: relative;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conversation-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.conversation-title strong {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.conversation-subtitle {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.conversation-last-message {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conversation-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 右侧聊天窗口 */
|
||||
.chat-window {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-header-info h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-header-info p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chat-header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-item.message-sent {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.message-sent .message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
word-break: break-word;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.message-sent .message-bubble {
|
||||
background-color: #2d8cf0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.interview-invitation {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: #fff9e6;
|
||||
border-left: 3px solid #ff9900;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #e8eaec;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chat-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chat-placeholder p {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.conversation-items::-webkit-scrollbar,
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.conversation-items::-webkit-scrollbar-thumb,
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background-color: #dcdee2;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.conversation-items::-webkit-scrollbar-track,
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
330
admin/src/views/chat/chat_records.vue
Normal file
330
admin/src/views/chat/chat_records.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增聊天记录</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="平台">
|
||||
<Select v-model="gridOption.param.seachOption.platform" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="boss">Boss直聘</Option>
|
||||
<Option value="liepin">猎聘</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="消息类型">
|
||||
<Select v-model="gridOption.param.seachOption.messageType" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="sent">发送</Option>
|
||||
<Option value="received">接收</Option>
|
||||
<Option value="system">系统</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import chatRecordsServer from '@/api/operation/chat_records_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["sn_code"] = [{ required: true, message: '请填写设备SN码', trigger: 'blur' }]
|
||||
rules["platform"] = [{ required: true, message: '请选择平台', trigger: 'change' }]
|
||||
rules["messageContent"] = [{ required: true, message: '请填写消息内容', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
replyContent: '',
|
||||
seachTypes: [
|
||||
{ key: 'companyName', value: '公司名称' },
|
||||
{ key: 'contactName', value: '联系人' },
|
||||
{ key: 'sn_code', value: '设备SN码' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'companyName',
|
||||
value: '',
|
||||
platform: null,
|
||||
messageType: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '设备SN码', key: 'sn_code', minWidth: 120 },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const platformMap = {
|
||||
'boss': { text: 'Boss直聘', color: 'blue' },
|
||||
'liepin': { text: '猎聘', color: 'green' }
|
||||
}
|
||||
const platform = platformMap[params.row.platform] || { text: params.row.platform, color: 'default' }
|
||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||
}
|
||||
},
|
||||
{ title: '岗位名称', key: 'jobTitle', minWidth: 150 },
|
||||
{ title: '公司名称', key: 'companyName', minWidth: 150 },
|
||||
{ title: '联系人', key: 'contactName', minWidth: 120 },
|
||||
{
|
||||
title: '消息类型',
|
||||
key: 'messageType',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const typeMap = {
|
||||
'sent': { text: '发送', color: 'blue' },
|
||||
'received': { text: '接收', color: 'success' },
|
||||
'system': { text: '系统', color: 'default' }
|
||||
}
|
||||
const type = typeMap[params.row.messageType] || { text: params.row.messageType, color: 'default' }
|
||||
return h('Tag', { props: { color: type.color } }, type.text)
|
||||
}
|
||||
},
|
||||
{ title: '消息内容', key: 'messageContent', minWidth: 250 },
|
||||
{ title: '发送时间', key: 'sendTime', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 300,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '查看详情',
|
||||
type: 'info',
|
||||
click: () => {
|
||||
this.showChatDetail(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '回复',
|
||||
type: 'success',
|
||||
click: () => {
|
||||
this.replyMessage(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '设备SN码', key: 'sn_code', type: 'text', required: true },
|
||||
{ title: '平台', key: 'platform', type: 'select', required: true, options: [
|
||||
{ value: 'boss', label: 'Boss直聘' },
|
||||
{ value: 'liepin', label: '猎聘' }
|
||||
]},
|
||||
{ title: '投递记录ID', key: 'applyId', type: 'text' },
|
||||
{ title: '岗位名称', key: 'jobTitle', type: 'text' },
|
||||
{ title: '公司名称', key: 'companyName', type: 'text' },
|
||||
{ title: '联系人', key: 'contactName', type: 'text' },
|
||||
{ title: '消息类型', key: 'messageType', type: 'select', options: [
|
||||
{ value: 'sent', label: '发送' },
|
||||
{ value: 'received', label: '接收' },
|
||||
{ value: 'system', label: '系统' }
|
||||
]},
|
||||
{ title: '消息内容', key: 'messageContent', type: 'textarea', required: true },
|
||||
{ title: '是否已读', key: 'isRead', type: 'switch' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
chatRecordsServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await chatRecordsServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
chatRecordsServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '聊天记录.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'companyName',
|
||||
value: '',
|
||||
platform: null,
|
||||
messageType: null
|
||||
}
|
||||
this.query(1)
|
||||
},
|
||||
showChatDetail(row) {
|
||||
this.$Modal.info({
|
||||
title: '聊天记录详情',
|
||||
width: 800,
|
||||
render: (h) => {
|
||||
return h('div', { style: { maxHeight: '500px', overflowY: 'auto' } }, [
|
||||
h('h3', { style: { marginBottom: '15px', color: '#2d8cf0' } }, '基本信息'),
|
||||
h('p', [h('strong', '设备SN码: '), row.sn_code || '未知']),
|
||||
h('p', [h('strong', '平台: '), row.platform === 'boss' ? 'Boss直聘' : row.platform === 'liepin' ? '猎聘' : row.platform]),
|
||||
h('p', [h('strong', '岗位名称: '), row.jobTitle || '未知']),
|
||||
h('p', [h('strong', '公司名称: '), row.companyName || '未知']),
|
||||
h('p', [h('strong', '联系人: '), row.contactName || '未知']),
|
||||
|
||||
h('h3', { style: { marginTop: '20px', marginBottom: '15px', color: '#2d8cf0' } }, '消息信息'),
|
||||
h('p', [h('strong', '消息类型: '), this.getMessageTypeText(row.messageType)]),
|
||||
h('p', [h('strong', '发送时间: '), row.sendTime || '未知']),
|
||||
h('p', [h('strong', '是否已读: '), row.isRead ? '是' : '否']),
|
||||
|
||||
h('div', { style: { marginTop: '20px' } }, [
|
||||
h('strong', '消息内容:'),
|
||||
h('div', {
|
||||
style: {
|
||||
whiteSpace: 'pre-wrap',
|
||||
marginTop: '10px',
|
||||
padding: '15px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e8e8e8'
|
||||
}
|
||||
}, row.messageContent)
|
||||
])
|
||||
])
|
||||
}
|
||||
})
|
||||
},
|
||||
replyMessage(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '回复消息',
|
||||
render: (h) => {
|
||||
return h('div', [
|
||||
h('p', { style: { marginBottom: '10px' } }, [
|
||||
h('strong', '回复给: '),
|
||||
`${row.companyName} - ${row.contactName || 'HR'}`
|
||||
]),
|
||||
h('p', { style: { marginBottom: '10px' } }, [
|
||||
h('strong', '原消息: '),
|
||||
row.messageContent
|
||||
]),
|
||||
h('Input', {
|
||||
props: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
placeholder: '请输入回复内容'
|
||||
},
|
||||
on: {
|
||||
input: (val) => {
|
||||
this.replyContent = val
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
},
|
||||
onOk: async () => {
|
||||
if (!this.replyContent) {
|
||||
this.$Message.warning('请输入回复内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loading = this.$Message.loading({
|
||||
content: '正在发送消息...',
|
||||
duration: 0
|
||||
})
|
||||
|
||||
const sn_code = row.sn_code || localStorage.getItem('current_sn_code') || 'GHJU'
|
||||
|
||||
await chatRecordsServer.sendMessage({
|
||||
sn_code: sn_code,
|
||||
jobId: row.jobId,
|
||||
content: this.replyContent,
|
||||
chatType: 'reply',
|
||||
platform: row.platform
|
||||
})
|
||||
|
||||
loading()
|
||||
this.$Message.success('消息发送成功!')
|
||||
this.replyContent = ''
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
this.$Message.error('消息发送失败:' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
getMessageTypeText(type) {
|
||||
const typeMap = {
|
||||
'sent': '发送',
|
||||
'received': '接收',
|
||||
'system': '系统'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
520
admin/src/views/home/index.vue
Normal file
520
admin/src/views/home/index.vue
Normal file
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="home-statistics">
|
||||
<!-- 设备选择器 -->
|
||||
<Card style="margin-bottom: 16px;">
|
||||
<p slot="title">设备选择</p>
|
||||
<Select
|
||||
v-model="selectedDeviceSn"
|
||||
@on-change="handleDeviceChange"
|
||||
placeholder="请选择设备"
|
||||
style="width: 300px;"
|
||||
>
|
||||
<Option
|
||||
v-for="device in deviceList"
|
||||
:value="device.deviceSn"
|
||||
:key="device.deviceSn || device.deviceName"
|
||||
>
|
||||
{{ device.deviceName || device.deviceSn }}
|
||||
</Option>
|
||||
</Select>
|
||||
</Card>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px;">
|
||||
<Col span="6">
|
||||
<Card>
|
||||
<div class="statistic-item">
|
||||
<div class="statistic-title">今日投递</div>
|
||||
<div class="statistic-value">{{ todayStats.applyCount }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span="6">
|
||||
<Card>
|
||||
<div class="statistic-item">
|
||||
<div class="statistic-title">今日找工作</div>
|
||||
<div class="statistic-value">{{ todayStats.jobSearchCount }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span="6">
|
||||
<Card>
|
||||
<div class="statistic-item">
|
||||
<div class="statistic-title">今日沟通</div>
|
||||
<div class="statistic-value">{{ todayStats.chatCount }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span="6">
|
||||
<Card>
|
||||
<div class="statistic-item">
|
||||
<div class="statistic-title">执行中任务</div>
|
||||
<div class="statistic-value">{{ todayStats.runningTaskCount }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 当前执行任务的命令区块(指令列表) -->
|
||||
<Card v-if="runningTasks.length > 0" style="margin-bottom: 16px;">
|
||||
<p slot="title">当前执行中的任务</p>
|
||||
<Table :columns="taskColumns" :data="runningTasks" :loading="taskLoading">
|
||||
<template slot-scope="{ row }" slot="commands">
|
||||
<Tag
|
||||
v-for="(cmd, index) in row.commands"
|
||||
:key="index"
|
||||
:color="getCommandColor(cmd.status)"
|
||||
style="margin: 2px;"
|
||||
>
|
||||
{{ cmd.commandName }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<!-- 趋势图表(7天趋势) -->
|
||||
<Card>
|
||||
<p slot="title">近7天统计趋势</p>
|
||||
<div ref="chartContainer" style="height: 400px;"></div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import DeviceStatusServer from '../../api/device/device_status_server.js'
|
||||
import StatisticsServer from '../../api/statistics/statistics_server.js'
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedDeviceSn: '',
|
||||
deviceList: [],
|
||||
todayStats: {
|
||||
applyCount: 0,
|
||||
jobSearchCount: 0,
|
||||
chatCount: 0,
|
||||
runningTaskCount: 0
|
||||
},
|
||||
chartData: {
|
||||
dates: [],
|
||||
applyData: [],
|
||||
jobSearchData: [],
|
||||
chatData: []
|
||||
},
|
||||
runningTasks: [],
|
||||
taskLoading: false,
|
||||
chartInstance: null, // ECharts实例
|
||||
taskColumns: [
|
||||
{
|
||||
title: '任务名称',
|
||||
key: 'taskName',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '任务类型',
|
||||
key: 'taskType',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
key: 'startTime',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
key: 'progress',
|
||||
width: 100,
|
||||
render: (h, params) => {
|
||||
return h('Progress', {
|
||||
props: {
|
||||
percent: params.row.progress || 0,
|
||||
status: 'active'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '命令列表',
|
||||
slot: 'commands',
|
||||
minWidth: 300
|
||||
}
|
||||
],
|
||||
refreshTimer: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 从 store 获取选中的设备
|
||||
storeDeviceSn() {
|
||||
return this.$store.getters['device/selectedDeviceSn']
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
storeDeviceSn: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val && val !== this.selectedDeviceSn) {
|
||||
this.selectedDeviceSn = val
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadDeviceList()
|
||||
// 每30秒刷新一次数据
|
||||
this.refreshTimer = setInterval(() => {
|
||||
this.loadData()
|
||||
}, 30000)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// 清理定时器
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer)
|
||||
this.refreshTimer = null
|
||||
}
|
||||
// 移除窗口大小调整监听
|
||||
if (this._chartResizeHandler) {
|
||||
window.removeEventListener('resize', this._chartResizeHandler)
|
||||
this._chartResizeHandler = null
|
||||
}
|
||||
// 销毁图表实例
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.dispose()
|
||||
this.chartInstance = null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载设备列表
|
||||
async loadDeviceList() {
|
||||
try {
|
||||
const res = await DeviceStatusServer.page({
|
||||
seachOption: {},
|
||||
pageOption: { page: 1, pageSize: 100 }
|
||||
})
|
||||
|
||||
console.log('设备列表接口返回:', res)
|
||||
console.log('res.data:', res.data)
|
||||
console.log('res.data.list:', res.data?.list)
|
||||
console.log('res.data.rows:', res.data?.rows)
|
||||
|
||||
// 支持多种数据格式
|
||||
const deviceList = res.data?.list || res.data?.rows || res.data?.data || []
|
||||
|
||||
console.log('解析后的设备列表:', deviceList)
|
||||
console.log('设备数量:', deviceList.length)
|
||||
|
||||
if (deviceList.length > 0) {
|
||||
// 统一设备字段名,去重处理
|
||||
const deviceMap = new Map()
|
||||
|
||||
deviceList.forEach(device => {
|
||||
const sn = device.deviceSn || device.sn_code || device.snCode || device.id
|
||||
if (sn && !deviceMap.has(sn)) {
|
||||
deviceMap.set(sn, {
|
||||
deviceSn: sn,
|
||||
deviceName: device.deviceName || device.device_name || device.name || sn
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.deviceList = Array.from(deviceMap.values())
|
||||
|
||||
console.log('处理后的设备列表:', this.deviceList)
|
||||
console.log('处理后设备数量:', this.deviceList.length)
|
||||
|
||||
this.$store.dispatch('device/setDeviceList', this.deviceList)
|
||||
|
||||
// 如果没有选中设备且有设备列表,选中第一个
|
||||
if (!this.selectedDeviceSn && this.deviceList.length > 0) {
|
||||
this.selectedDeviceSn = this.deviceList[0].deviceSn
|
||||
this.$store.dispatch('device/setSelectedDevice', this.selectedDeviceSn)
|
||||
// 立即加载数据
|
||||
this.loadData()
|
||||
} else if (this.selectedDeviceSn) {
|
||||
// 如果已有选中设备,直接加载数据
|
||||
this.loadData()
|
||||
}
|
||||
} else {
|
||||
console.warn('设备列表为空,原始数据:', res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
this.$Message.error('加载设备列表失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
},
|
||||
|
||||
// 设备切换
|
||||
handleDeviceChange(deviceSn) {
|
||||
this.$store.dispatch('device/setSelectedDevice', deviceSn)
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 加载所有数据
|
||||
async loadData() {
|
||||
if (!this.selectedDeviceSn) return
|
||||
|
||||
await Promise.all([
|
||||
this.loadTodayStats(),
|
||||
this.loadChartData(),
|
||||
this.loadRunningTasks()
|
||||
])
|
||||
},
|
||||
|
||||
// 加载今日统计
|
||||
async loadTodayStats() {
|
||||
try {
|
||||
console.log('加载今日统计,设备SN:', this.selectedDeviceSn)
|
||||
const res = await StatisticsServer.getOverview(this.selectedDeviceSn)
|
||||
console.log('今日统计接口返回:', res)
|
||||
|
||||
if (res.code === 0) {
|
||||
this.todayStats = {
|
||||
applyCount: res.data?.applyCount || res.data?.todayApplyCount || 0,
|
||||
jobSearchCount: res.data?.jobSearchCount || res.data?.todayJobSearchCount || 0,
|
||||
chatCount: res.data?.chatCount || res.data?.todayChatCount || 0,
|
||||
runningTaskCount: res.data?.runningTaskCount || res.data?.runningTasks || 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载今日统计失败:', error)
|
||||
// 不显示错误,避免干扰用户
|
||||
}
|
||||
},
|
||||
|
||||
// 加载图表数据
|
||||
async loadChartData() {
|
||||
try {
|
||||
console.log('加载图表数据,设备SN:', this.selectedDeviceSn)
|
||||
const res = await StatisticsServer.getDailyStatistics({
|
||||
deviceSn: this.selectedDeviceSn,
|
||||
days: 7
|
||||
})
|
||||
console.log('图表数据接口返回:', res)
|
||||
|
||||
if (res.code === 0) {
|
||||
this.chartData = {
|
||||
dates: res.data?.dates || [],
|
||||
applyData: res.data?.applyData || [],
|
||||
jobSearchData: res.data?.jobSearchData || [],
|
||||
chatData: res.data?.chatData || []
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.renderChart()
|
||||
})
|
||||
} else {
|
||||
console.warn('加载图表数据失败:', res.msg || res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载图表数据失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载正在执行的任务
|
||||
async loadRunningTasks() {
|
||||
this.taskLoading = true
|
||||
try {
|
||||
console.log('加载执行中任务,设备SN:', this.selectedDeviceSn)
|
||||
const res = await StatisticsServer.getRunningTasks(this.selectedDeviceSn)
|
||||
console.log('执行中任务接口返回:', res)
|
||||
|
||||
if (res.code === 0) {
|
||||
this.runningTasks = res.data || []
|
||||
} else {
|
||||
console.warn('加载执行中任务失败:', res.msg || res.message)
|
||||
this.runningTasks = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载执行中任务失败:', error)
|
||||
this.runningTasks = []
|
||||
} finally {
|
||||
this.taskLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染图表
|
||||
renderChart() {
|
||||
if (!this.$refs.chartContainer) return
|
||||
|
||||
// 如果没有数据,显示提示
|
||||
if (!this.chartData.dates || this.chartData.dates.length === 0) {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.dispose()
|
||||
this.chartInstance = null
|
||||
}
|
||||
this.$refs.chartContainer.innerHTML = `
|
||||
<div style="text-align: center; padding: 100px 0; color: #999;">
|
||||
<p>暂无统计数据</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化或获取图表实例
|
||||
if (!this.chartInstance) {
|
||||
this.chartInstance = echarts.init(this.$refs.chartContainer)
|
||||
}
|
||||
|
||||
// 格式化日期显示(只显示月-日)
|
||||
const formattedDates = this.chartData.dates.map(date => {
|
||||
const d = new Date(date)
|
||||
return `${d.getMonth() + 1}-${d.getDate()}`
|
||||
})
|
||||
|
||||
// 配置图表选项
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['投递', '找工作', '沟通'],
|
||||
top: 10
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: formattedDates,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '投递',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: this.chartData.applyData,
|
||||
itemStyle: {
|
||||
color: '#2d8cf0'
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(45, 140, 240, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(45, 140, 240, 0.1)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '找工作',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: this.chartData.jobSearchData,
|
||||
itemStyle: {
|
||||
color: '#19be6b'
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(25, 190, 107, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(25, 190, 107, 0.1)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '沟通',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: this.chartData.chatData,
|
||||
itemStyle: {
|
||||
color: '#ff9900'
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(255, 153, 0, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(255, 153, 0, 0.1)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 设置图表选项
|
||||
this.chartInstance.setOption(option, true)
|
||||
|
||||
// 响应式调整 - 使用箭头函数确保this指向正确
|
||||
if (!this._chartResizeHandler) {
|
||||
this._chartResizeHandler = () => {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.resize()
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', this._chartResizeHandler)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取命令状态颜色
|
||||
getCommandColor(status) {
|
||||
const colorMap = {
|
||||
'pending': 'default',
|
||||
'running': 'blue',
|
||||
'success': 'green',
|
||||
'failed': 'red',
|
||||
'skipped': 'warning'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-statistics {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.statistic-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statistic-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statistic-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #2d8cf0;
|
||||
}
|
||||
</style>
|
||||
215
admin/src/views/system/system_config.vue
Normal file
215
admin/src/views/system/system_config.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增配置</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="配置分类">
|
||||
<Select v-model="gridOption.param.seachOption.category" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="system">系统配置</Option>
|
||||
<Option value="ai">AI服务</Option>
|
||||
<Option value="mqtt">MQTT</Option>
|
||||
<Option value="schedule">调度</Option>
|
||||
<Option value="platform">平台</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select v-model="gridOption.param.seachOption.isActive" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option :value="true">启用</Option>
|
||||
<Option :value="false">禁用</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import systemConfigServer from '@/api/system/system_config_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["configKey"] = [{ required: true, message: '请填写配置键', trigger: 'blur' }]
|
||||
rules["configValue"] = [{ required: true, message: '请填写配置值', trigger: 'blur' }]
|
||||
rules["configName"] = [{ required: true, message: '请填写配置名称', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'configKey', value: '配置键' },
|
||||
{ key: 'configName', value: '配置名称' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'configKey',
|
||||
value: '',
|
||||
category: null,
|
||||
isActive: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '配置键', key: 'configKey', minWidth: 200 },
|
||||
{ title: '配置名称', key: 'configName', minWidth: 150 },
|
||||
{
|
||||
title: '配置分类',
|
||||
key: 'category',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const categoryMap = {
|
||||
'system': { text: '系统配置', color: 'blue' },
|
||||
'ai': { text: 'AI服务', color: 'purple' },
|
||||
'mqtt': { text: 'MQTT', color: 'green' },
|
||||
'schedule': { text: '调度', color: 'orange' },
|
||||
'platform': { text: '平台', color: 'cyan' }
|
||||
}
|
||||
const category = categoryMap[params.row.category] || { text: params.row.category, color: 'default' }
|
||||
return h('Tag', { props: { color: category.color } }, category.text)
|
||||
}
|
||||
},
|
||||
{ title: '配置值', key: 'configValue', minWidth: 200 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'isActive',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.isActive ? 'success' : 'default' }
|
||||
}, params.row.isActive ? '启用' : '禁用')
|
||||
}
|
||||
},
|
||||
{ title: '描述', key: 'description', minWidth: 200 },
|
||||
{ title: '更新时间', key: 'last_modify_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '配置键', key: 'configKey', type: 'text', required: true },
|
||||
{ title: '配置名称', key: 'configName', type: 'text', required: true },
|
||||
{ title: '配置分类', key: 'category', type: 'select', options: [
|
||||
{ value: 'system', label: '系统配置' },
|
||||
{ value: 'ai', label: 'AI服务' },
|
||||
{ value: 'mqtt', label: 'MQTT' },
|
||||
{ value: 'schedule', label: '调度' },
|
||||
{ value: 'platform', label: '平台' }
|
||||
]},
|
||||
{ title: '配置值', key: 'configValue', type: 'textarea', required: true },
|
||||
{ title: '默认值', key: 'defaultValue', type: 'text' },
|
||||
{ title: '数据类型', key: 'valueType', type: 'select', options: [
|
||||
{ value: 'string', label: '字符串' },
|
||||
{ value: 'number', label: '数字' },
|
||||
{ value: 'boolean', label: '布尔值' },
|
||||
{ value: 'json', label: 'JSON对象' }
|
||||
]},
|
||||
{ title: '描述', key: 'description', type: 'textarea' },
|
||||
{ title: '状态', key: 'isActive', type: 'switch' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
systemConfigServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await systemConfigServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
systemConfigServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '系统配置.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'configKey',
|
||||
value: '',
|
||||
category: null,
|
||||
isActive: null
|
||||
}
|
||||
this.query(1)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
0
admin/src/views/task/chat_records.vue
Normal file
0
admin/src/views/task/chat_records.vue
Normal file
415
admin/src/views/task/components/CommandsList.vue
Normal file
415
admin/src/views/task/components/CommandsList.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="commands-list-container commands-body">
|
||||
<!-- 头部导航 -->
|
||||
<div class="commands-header">
|
||||
<Button type="default" icon="ios-arrow-back" class="back-btn" @click="handleBack">
|
||||
返回任务列表
|
||||
</Button>
|
||||
<div class="commands-title">
|
||||
<Icon type="ios-list-box" size="20" />
|
||||
<span class="title-text">{{ title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<tables :columns="columns" :value="data" :loading="loading"></tables>
|
||||
|
||||
<!-- 查看完整内容弹窗 -->
|
||||
<Modal
|
||||
v-model="showDetailModal"
|
||||
title="查看完整内容"
|
||||
width="800"
|
||||
:mask-closable="true"
|
||||
>
|
||||
<div class="detail-content">
|
||||
<pre class="detail-pre">{{ detailContent }}</pre>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<Button @click="showDetailModal = false">关闭</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CommandsList',
|
||||
props: {
|
||||
// 是否显示
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '指令列表'
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 数据
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDetailModal: false,
|
||||
detailContent: '',
|
||||
columns: [
|
||||
{ title: 'ID', key: 'id', align: 'center' },
|
||||
{ title: '序号', key: 'sequence', align: 'center' },
|
||||
{
|
||||
title: '指令类型',
|
||||
key: 'command_type',
|
||||
align: 'center',
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: {
|
||||
color: this.getCommandTypeColor(params.row.command_type)
|
||||
}
|
||||
}, params.row.command_type)
|
||||
}
|
||||
},
|
||||
{ title: '指令名称', key: 'command_name' },
|
||||
{
|
||||
title: '指令参数',
|
||||
key: 'command_params',
|
||||
type: 'template',
|
||||
width: 250,
|
||||
render: (h, params) => {
|
||||
if (!params.row.command_params) {
|
||||
return h('span', '-')
|
||||
}
|
||||
return h('div', {
|
||||
class: 'json-content',
|
||||
style: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
on: {
|
||||
dblclick: () => {
|
||||
this.showDetail('指令参数', params.row.command_params)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('pre', this.formatJson(params.row.command_params))
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: {
|
||||
color: this.getStatusColor(params.row.status)
|
||||
}
|
||||
}, this.getStatusText(params.row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '执行结果',
|
||||
key: 'result',
|
||||
type: 'template',
|
||||
width: 300,
|
||||
render: (h, params) => {
|
||||
if (!params.row.result) {
|
||||
return h('span', '-')
|
||||
}
|
||||
return h('div', {
|
||||
class: 'json-content',
|
||||
style: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
on: {
|
||||
dblclick: () => {
|
||||
this.showDetail('执行结果', params.row.result)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('pre', this.formatJson(params.row.result))
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '错误信息',
|
||||
key: 'error_message',
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
if (!params.row.error_message) {
|
||||
return h('span', '-')
|
||||
}
|
||||
return h('div', {
|
||||
class: 'error-content'
|
||||
}, params.row.error_message)
|
||||
}
|
||||
},
|
||||
{ title: '重试次数', key: 'retry_count', align: 'center' },
|
||||
{ title: '执行时长(s)', key: 'execution_time', align: 'center' },
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'create_time',
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
return h('span', this.formatDateTime(params.row.create_time))
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
key: 'last_modify_time',
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
return h('span', this.formatDateTime(params.row.last_modify_time))
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 返回按钮点击
|
||||
handleBack() {
|
||||
this.$emit('back')
|
||||
},
|
||||
|
||||
// 获取指令类型颜色
|
||||
getCommandTypeColor(type) {
|
||||
const colorMap = {
|
||||
'browser': 'blue',
|
||||
'search': 'cyan',
|
||||
'fill': 'geekblue',
|
||||
'click': 'purple',
|
||||
'wait': 'orange',
|
||||
'screenshot': 'magenta',
|
||||
'extract': 'green'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
},
|
||||
|
||||
// 获取状态颜色
|
||||
getStatusColor(status) {
|
||||
const colorMap = {
|
||||
'pending': 'default',
|
||||
'running': 'blue',
|
||||
'success': 'success',
|
||||
'failed': 'error',
|
||||
'skipped': 'warning'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const textMap = {
|
||||
'pending': '待执行',
|
||||
'running': '执行中',
|
||||
'success': '成功',
|
||||
'failed': '失败',
|
||||
'skipped': '已跳过'
|
||||
}
|
||||
return textMap[status] || status
|
||||
},
|
||||
|
||||
// 格式化JSON
|
||||
formatJson(str) {
|
||||
if (!str) return ''
|
||||
try {
|
||||
const obj = typeof str === 'string' ? JSON.parse(str) : str
|
||||
return JSON.stringify(obj, null, 2)
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期时间
|
||||
formatDateTime(datetime) {
|
||||
if (!datetime) return '-'
|
||||
const date = new Date(datetime)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
},
|
||||
|
||||
// 显示完整内容
|
||||
showDetail(title, content) {
|
||||
this.detailContent = this.formatJson(content)
|
||||
this.showDetailModal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器 - 撑满父元素 */
|
||||
.commands-list-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 头部导航 */
|
||||
.commands-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.commands-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.commands-title>>>.ivu-icon {
|
||||
margin-right: 8px;
|
||||
color: #2d8cf0;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
color: #17233d;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* JSON内容显示 */
|
||||
.commands-body>>>.json-content {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.commands-body>>>.json-content:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.commands-body>>>.json-content pre {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
background: #f8f8f9;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
color: #515a6e;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 错误信息显示 */
|
||||
.error-content {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #ed4014;
|
||||
padding: 8px;
|
||||
background: #fff1f0;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #ed4014;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
.commands-body>>>.ivu-table {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.commands-body>>>.ivu-table-wrapper {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.commands-body>>>.ivu-table-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.commands-body>>>.ivu-table th {
|
||||
background: #f8f8f9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.commands-body>>>.ivu-table-stripe .ivu-table-body tr:nth-child(2n) td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.commands-body>>>.ivu-table td {
|
||||
padding: 12px 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* 确保表格单元格内容不会撑高 */
|
||||
.commands-body>>>.ivu-table td .json-content {
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.commands-body>>>.ivu-table td .json-content pre {
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 滚动条美化 - 仅作用于外层容器 */
|
||||
.commands-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.commands-body::-webkit-scrollbar-thumb {
|
||||
background: #dcdee2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.commands-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #b8bbbf;
|
||||
}
|
||||
|
||||
.commands-body::-webkit-scrollbar-track {
|
||||
background: #f8f8f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 弹窗内容样式 */
|
||||
.detail-content {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.detail-pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: #f8f8f9;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #515a6e;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
279
admin/src/views/task/components/README.md
Normal file
279
admin/src/views/task/components/README.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# CommandsList 组件
|
||||
|
||||
## 📦 组件说明
|
||||
|
||||
`CommandsList.vue` 是一个通用的指令列表展示组件,专门用于在任务状态页面中展示任务的执行指令详情。
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- ✅ **撑满父元素** - 组件宽高自动撑满父容器(100% width & height)
|
||||
- ✅ **自适应表格** - 表格列宽自动分配,无需手动设置每列宽度
|
||||
- ✅ **返回导航** - 内置返回按钮,支持返回上级页面
|
||||
- ✅ **加载状态** - 支持loading状态显示
|
||||
- ✅ **美观样式** - 现代化UI设计,带有斑马纹、边框等
|
||||
- ✅ **数据格式化** - 自动格式化JSON、时间等数据
|
||||
- ✅ **状态标签** - 彩色标签区分不同状态和类型
|
||||
- ✅ **独立封装** - 完全独立的组件,可在其他页面复用
|
||||
|
||||
## 📋 Props 参数
|
||||
|
||||
| 参数名 | 类型 | 默认值 | 必填 | 说明 |
|
||||
|--------|------|--------|------|------|
|
||||
| visible | Boolean | false | 否 | 是否显示组件 |
|
||||
| title | String | '指令列表' | 否 | 页面标题 |
|
||||
| loading | Boolean | false | 否 | 数据加载状态 |
|
||||
| data | Array | [] | 否 | 指令列表数据 |
|
||||
|
||||
## 🎯 Events 事件
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| back | 无 | 点击返回按钮时触发 |
|
||||
|
||||
## 💻 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 列表页面 -->
|
||||
<div v-show="!showDetail">
|
||||
<Button @click="handleShowDetail">查看详情</Button>
|
||||
</div>
|
||||
|
||||
<!-- 详情页面 - 使用 CommandsList 组件 -->
|
||||
<CommandsList
|
||||
v-show="showDetail"
|
||||
:visible="showDetail"
|
||||
:title="detailTitle"
|
||||
:loading="detailLoading"
|
||||
:data="detailData"
|
||||
@back="handleBack" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CommandsList from './components/CommandsList.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommandsList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDetail: false,
|
||||
detailTitle: '指令列表',
|
||||
detailLoading: false,
|
||||
detailData: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleShowDetail() {
|
||||
this.showDetail = true
|
||||
this.detailLoading = true
|
||||
|
||||
try {
|
||||
// 获取数据
|
||||
const res = await this.fetchData()
|
||||
this.detailData = res.data
|
||||
} catch (error) {
|
||||
this.$Message.error('获取数据失败')
|
||||
} finally {
|
||||
this.detailLoading = false
|
||||
}
|
||||
},
|
||||
handleBack() {
|
||||
this.showDetail = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 在 task_status.vue 中的实际应用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<!-- 任务列表主页面 -->
|
||||
<div v-show="!showCommandsView" class="task-list-view">
|
||||
<!-- 任务列表内容 -->
|
||||
</div>
|
||||
|
||||
<!-- 指令列表详情页面 -->
|
||||
<CommandsList
|
||||
v-show="showCommandsView"
|
||||
:visible="showCommandsView"
|
||||
:title="commandsModal.title"
|
||||
:loading="commandsModal.loading"
|
||||
:data="commandsModal.data"
|
||||
@back="backToTaskList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CommandsList from './components/CommandsList.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommandsList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCommandsView: false,
|
||||
commandsModal: {
|
||||
loading: false,
|
||||
title: '指令列表',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async showCommands(row) {
|
||||
this.showCommandsView = true
|
||||
this.commandsModal.loading = true
|
||||
this.commandsModal.title = `指令列表 - 任务ID: ${row.id} (${row.taskName})`
|
||||
|
||||
try {
|
||||
const res = await taskStatusServer.getCommands(row.id)
|
||||
this.commandsModal.data = res.data || []
|
||||
} catch (error) {
|
||||
this.$Message.error('获取指令列表失败')
|
||||
this.commandsModal.data = []
|
||||
} finally {
|
||||
this.commandsModal.loading = false
|
||||
}
|
||||
},
|
||||
backToTaskList() {
|
||||
this.showCommandsView = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 📊 数据格式
|
||||
|
||||
### 指令数据结构(data 数组元素)
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 123, // 指令ID
|
||||
sequence: 1, // 序号
|
||||
command_type: 'browser', // 指令类型:browser/search/fill/click/wait/screenshot/extract
|
||||
command_name: '打开浏览器', // 指令名称
|
||||
command_params: '{"url":"..."}', // 指令参数(JSON字符串)
|
||||
status: 'success', // 状态:pending/running/success/failed/skipped
|
||||
result: '{"data":"..."}', // 执行结果(JSON字符串)
|
||||
error_message: null, // 错误信息
|
||||
retry_count: 0, // 重试次数
|
||||
execution_time: 1.23, // 执行时长(秒)
|
||||
create_time: '2025-10-29 10:00:00', // 创建时间
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 样式特性
|
||||
|
||||
### 布局特性
|
||||
- **弹性布局** - 使用 flexbox 实现头部+内容区域布局
|
||||
- **100%撑满** - 宽高100%,自动撑满父容器
|
||||
- **自适应列宽** - 表格列宽自动分配,无需手动设置
|
||||
|
||||
### 视觉效果
|
||||
- **现代化头部** - 带返回按钮和标题的导航栏
|
||||
- **美观表格** - 斑马纹、边框、悬停效果
|
||||
- **彩色标签** - 不同状态使用不同颜色区分
|
||||
- **格式化显示** - JSON自动格式化,代码高亮
|
||||
- **错误高亮** - 错误信息红色背景突出显示
|
||||
- **滚动优化** - 美化的滚动条样式
|
||||
|
||||
### 响应式
|
||||
- 表格自动适应父容器宽度
|
||||
- 内容区域独立滚动
|
||||
- 头部固定不滚动
|
||||
|
||||
## 🔧 自定义样式
|
||||
|
||||
如果需要自定义样式,可以在父组件中使用深度选择器:
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* 修改组件内部样式 */
|
||||
.my-page >>> .commands-list-container {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 修改表格样式 */
|
||||
.my-page >>> .commands-body .ivu-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 📌 注意事项
|
||||
|
||||
1. **父容器要求** - 父容器必须有明确的宽高,否则组件无法正确撑满
|
||||
2. **v-show控制** - 建议使用 `v-show` 而不是 `v-if`,以保持状态
|
||||
3. **数据格式** - JSON字段支持字符串和对象两种格式,组件会自动处理
|
||||
4. **时间格式** - 时间字段应为标准的日期时间字符串
|
||||
5. **状态映射** - 组件内部已定义状态和类型的颜色映射,可根据需要修改
|
||||
|
||||
## 🚀 特色功能
|
||||
|
||||
### 1. 自动JSON格式化
|
||||
组件会自动格式化 `command_params` 和 `result` 字段的JSON数据,提供更好的可读性。
|
||||
|
||||
### 2. 智能状态标签
|
||||
根据不同的指令类型和状态,自动显示对应颜色的标签:
|
||||
|
||||
**指令类型颜色**:
|
||||
- browser - 蓝色
|
||||
- search - 青色
|
||||
- fill - 深蓝色
|
||||
- click - 紫色
|
||||
- wait - 橙色
|
||||
- screenshot - 洋红色
|
||||
- extract - 绿色
|
||||
|
||||
**状态颜色**:
|
||||
- pending(待执行) - 灰色
|
||||
- running(执行中) - 蓝色
|
||||
- success(成功) - 绿色
|
||||
- failed(失败) - 红色
|
||||
- skipped(已跳过) - 橙色
|
||||
|
||||
### 3. 错误信息高亮
|
||||
错误信息使用红色背景和左边框高亮显示,便于快速定位问题。
|
||||
|
||||
### 4. 时间格式化
|
||||
自动格式化时间为 `YYYY-MM-DD HH:mm:ss` 格式,统一时间显示样式。
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0.0 (2025-10-29)
|
||||
- ✅ 初始版本发布
|
||||
- ✅ 实现基础功能
|
||||
- ✅ 移除所有列宽限制,实现表格自动撑满
|
||||
- ✅ 优化样式和交互
|
||||
- ✅ 完善组件文档
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
如需改进此组件,请遵循以下原则:
|
||||
1. 保持组件的通用性和独立性
|
||||
2. 确保样式不依赖外部全局样式
|
||||
3. 维护良好的代码注释
|
||||
4. 更新此文档说明
|
||||
|
||||
## 📄 许可
|
||||
|
||||
此组件为项目内部组件,仅供项目内使用。
|
||||
|
||||
324
admin/src/views/task/task_status.vue
Normal file
324
admin/src/views/task/task_status.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<!-- 任务列表主页面 -->
|
||||
<div v-show="!showCommandsView" class="task-list-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增任务</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="任务类型">
|
||||
<Select v-model="gridOption.param.seachOption.taskType" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="search">搜索岗位</Option>
|
||||
<Option value="apply">投递简历</Option>
|
||||
<Option value="chat">聊天回复</Option>
|
||||
<Option value="follow">跟进</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="任务状态">
|
||||
<Select v-model="gridOption.param.seachOption.status" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="pending">待执行</Option>
|
||||
<Option value="running">执行中</Option>
|
||||
<Option value="success">成功</Option>
|
||||
<Option value="failed">失败</Option>
|
||||
<Option value="cancelled">已取消</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指令列表详情页面 -->
|
||||
<CommandsList
|
||||
v-show="showCommandsView"
|
||||
:visible="showCommandsView"
|
||||
:title="commandsModal.title"
|
||||
:loading="commandsModal.loading"
|
||||
:data="commandsModal.data"
|
||||
@back="backToTaskList" />
|
||||
|
||||
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import taskStatusServer from '@/api/operation/task_status_server.js'
|
||||
import CommandsList from './components/CommandsList.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommandsList
|
||||
},
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["taskType"] = [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
rules["sn_code"] = [{ required: true, message: '请填写设备SN码', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'taskName', value: '任务名称' },
|
||||
{ key: 'sn_code', value: '设备SN码' },
|
||||
{ key: 'id', value: '任务ID' }
|
||||
],
|
||||
showCommandsView: false, // 是否显示指令列表视图
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'taskName',
|
||||
value: '',
|
||||
taskType: null,
|
||||
status: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: '任务ID', key: 'id', minWidth: 180 },
|
||||
{ title: '设备SN码', key: 'sn_code', minWidth: 120 },
|
||||
{
|
||||
title: '任务类型',
|
||||
key: 'taskType',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const typeMap = {
|
||||
'search': { text: '搜索岗位', color: 'blue' },
|
||||
'apply': { text: '投递简历', color: 'success' },
|
||||
'chat': { text: '聊天回复', color: 'purple' },
|
||||
'follow': { text: '跟进', color: 'orange' }
|
||||
}
|
||||
const type = typeMap[params.row.taskType] || { text: params.row.taskType, color: 'default' }
|
||||
return h('Tag', { props: { color: type.color } }, type.text)
|
||||
}
|
||||
},
|
||||
{ title: '任务名称', key: 'taskName', minWidth: 150 },
|
||||
{
|
||||
title: '任务状态',
|
||||
key: 'status',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const statusMap = {
|
||||
'pending': { text: '待执行', color: 'default' },
|
||||
'running': { text: '执行中', color: 'blue' },
|
||||
'success': { text: '成功', color: 'success' },
|
||||
'failed': { text: '失败', color: 'error' },
|
||||
'cancelled': { text: '已取消', color: 'warning' }
|
||||
}
|
||||
const status = statusMap[params.row.status] || { text: params.row.status, color: 'default' }
|
||||
return h('Tag', { props: { color: status.color } }, status.text)
|
||||
}
|
||||
},
|
||||
{ title: '进度', key: 'progress', minWidth: 100 },
|
||||
{ title: '开始时间', key: 'startTime', minWidth: 150 },
|
||||
{ title: '结束时间', key: 'endTime', minWidth: 150 },
|
||||
{ title: '创建时间', key: 'create_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 360,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '指令列表',
|
||||
type: 'info',
|
||||
click: () => {
|
||||
this.showCommands(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
if (params.row.status === 'failed') {
|
||||
btns.push({
|
||||
title: '重试',
|
||||
type: 'success',
|
||||
click: () => {
|
||||
this.retryTask(params.row)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (params.row.status === 'pending' || params.row.status === 'running') {
|
||||
btns.push({
|
||||
title: '取消',
|
||||
type: 'warning',
|
||||
click: () => {
|
||||
this.cancelTask(params.row)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
btns.push({
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
})
|
||||
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '设备SN码', key: 'sn_code', type: 'text', required: true },
|
||||
{ title: '任务类型', key: 'taskType', type: 'select', required: true, options: [
|
||||
{ value: 'search', label: '搜索岗位' },
|
||||
{ value: 'apply', label: '投递简历' },
|
||||
{ value: 'chat', label: '聊天回复' },
|
||||
{ value: 'follow', label: '跟进' }
|
||||
]},
|
||||
{ title: '任务名称', key: 'taskName', type: 'text' },
|
||||
{ title: '任务描述', key: 'taskDescription', type: 'textarea' },
|
||||
{ title: '任务参数', key: 'taskParams', type: 'textarea' },
|
||||
{ title: '优先级', key: 'priority', type: 'number' },
|
||||
{ title: '最大重试次数', key: 'maxRetries', type: 'number' },
|
||||
{ title: '计划执行时间', key: 'scheduledTime', type: 'datetime' }
|
||||
],
|
||||
commandsModal: {
|
||||
loading: false,
|
||||
title: '指令列表',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
taskStatusServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
cancelTask(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认取消任务',
|
||||
content: `确定要取消任务 "${row.taskName}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await taskStatusServer.cancel(row)
|
||||
this.$Message.success('任务取消成功!')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
this.$Message.error('任务取消失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
retryTask(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认重试任务',
|
||||
content: `确定要重试任务 "${row.taskName}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await taskStatusServer.retry(row)
|
||||
this.$Message.success('任务重试成功!')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
this.$Message.error('任务重试失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await taskStatusServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
taskStatusServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '任务状态.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'taskName',
|
||||
value: '',
|
||||
taskType: null,
|
||||
status: null
|
||||
}
|
||||
this.query(1)
|
||||
},
|
||||
async showCommands(row) {
|
||||
this.showCommandsView = true
|
||||
this.commandsModal.loading = true
|
||||
this.commandsModal.title = `指令列表 - 任务ID: ${row.id} (${row.taskName})`
|
||||
|
||||
try {
|
||||
const res = await taskStatusServer.getCommands(row.id)
|
||||
this.commandsModal.data = res.data || []
|
||||
} catch (error) {
|
||||
this.$Message.error('获取指令列表失败')
|
||||
this.commandsModal.data = []
|
||||
} finally {
|
||||
this.commandsModal.loading = false
|
||||
}
|
||||
},
|
||||
backToTaskList() {
|
||||
this.showCommandsView = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 任务列表视图 */
|
||||
.task-list-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
335
admin/src/views/work/apply_records.vue
Normal file
335
admin/src/views/work/apply_records.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增投递记录</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="平台">
|
||||
<Select v-model="gridOption.param.seachOption.platform" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="boss">Boss直聘</Option>
|
||||
<Option value="liepin">猎聘</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="投递状态">
|
||||
<Select v-model="gridOption.param.seachOption.applyStatus" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="pending">待投递</Option>
|
||||
<Option value="applying">投递中</Option>
|
||||
<Option value="success">投递成功</Option>
|
||||
<Option value="failed">投递失败</Option>
|
||||
<Option value="duplicate">重复投递</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="反馈状态">
|
||||
<Select v-model="gridOption.param.seachOption.feedbackStatus" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="none">无反馈</Option>
|
||||
<Option value="viewed">已查看</Option>
|
||||
<Option value="interested">感兴趣</Option>
|
||||
<Option value="not_suitable">不合适</Option>
|
||||
<Option value="interview">面试邀约</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import applyRecordsServer from '@/api/work/apply_records_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["jobTitle"] = [{ required: true, message: '请填写岗位名称', trigger: 'blur' }]
|
||||
rules["companyName"] = [{ required: true, message: '请填写公司名称', trigger: 'blur' }]
|
||||
rules["platform"] = [{ required: true, message: '请选择平台', trigger: 'change' }]
|
||||
rules["applyStatus"] = [{ required: true, message: '请选择投递状态', trigger: 'change' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'jobTitle', value: '岗位名称' },
|
||||
{ key: 'companyName', value: '公司名称' },
|
||||
{ key: 'sn_code', value: '设备SN码' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'jobTitle',
|
||||
value: '',
|
||||
platform: null,
|
||||
applyStatus: null,
|
||||
feedbackStatus: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '设备SN码', key: 'sn_code', minWidth: 120 },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const platformMap = {
|
||||
'boss': { text: 'Boss直聘', color: 'blue' },
|
||||
'liepin': { text: '猎聘', color: 'green' }
|
||||
}
|
||||
const platform = platformMap[params.row.platform] || { text: params.row.platform, color: 'default' }
|
||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||
}
|
||||
},
|
||||
{ title: '岗位名称', key: 'jobTitle', minWidth: 180 },
|
||||
{ title: '公司名称', key: 'companyName', minWidth: 180 },
|
||||
{ title: '薪资', key: 'salary', minWidth: 120 },
|
||||
{ title: '地点', key: 'location', minWidth: 120 },
|
||||
{
|
||||
title: '投递状态',
|
||||
key: 'applyStatus',
|
||||
minWidth: 120,
|
||||
render: (h, params) => {
|
||||
const statusMap = {
|
||||
'pending': { text: '待投递', color: 'default' },
|
||||
'applying': { text: '投递中', color: 'blue' },
|
||||
'success': { text: '投递成功', color: 'success' },
|
||||
'failed': { text: '投递失败', color: 'error' },
|
||||
'duplicate': { text: '重复投递', color: 'warning' }
|
||||
}
|
||||
const status = statusMap[params.row.applyStatus] || { text: params.row.applyStatus, color: 'default' }
|
||||
return h('Tag', { props: { color: status.color } }, status.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '反馈状态',
|
||||
key: 'feedbackStatus',
|
||||
minWidth: 120,
|
||||
render: (h, params) => {
|
||||
const statusMap = {
|
||||
'none': { text: '无反馈', color: 'default' },
|
||||
'viewed': { text: '已查看', color: 'blue' },
|
||||
'interested': { text: '感兴趣', color: 'success' },
|
||||
'not_suitable': { text: '不合适', color: 'warning' },
|
||||
'interview': { text: '面试邀约', color: 'success' }
|
||||
}
|
||||
const status = statusMap[params.row.feedbackStatus] || { text: params.row.feedbackStatus, color: 'default' }
|
||||
return h('Tag', { props: { color: status.color } }, status.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '匹配度',
|
||||
key: 'matchScore',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const score = params.row.matchScore || 0
|
||||
const color = score >= 80 ? 'success' : score >= 60 ? 'warning' : 'error'
|
||||
return h('Tag', { props: { color: color } }, `${score}%`)
|
||||
}
|
||||
},
|
||||
{ title: '投递时间', key: 'applyTime', minWidth: 250 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 280,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '查看详情',
|
||||
type: 'info',
|
||||
click: () => {
|
||||
this.showApplyDetail(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '设备SN码', key: 'sn_code', type: 'text', required: false },
|
||||
{ title: '平台', key: 'platform', type: 'select', required: true, options: [
|
||||
{ value: 'boss', label: 'Boss直聘' },
|
||||
{ value: 'liepin', label: '猎聘' }
|
||||
]},
|
||||
{ title: '岗位ID', key: 'jobId', type: 'text', required: false },
|
||||
{ title: '岗位名称', key: 'jobTitle', type: 'text', required: true },
|
||||
{ title: '公司名称', key: 'companyName', type: 'text', required: true },
|
||||
{ title: '薪资', key: 'salary', type: 'text' },
|
||||
{ title: '地点', key: 'location', type: 'text' },
|
||||
{ title: '岗位链接', key: 'jobUrl', type: 'text' },
|
||||
{ title: '投递状态', key: 'applyStatus', type: 'select', required: true, options: [
|
||||
{ value: 'pending', label: '待投递' },
|
||||
{ value: 'applying', label: '投递中' },
|
||||
{ value: 'success', label: '投递成功' },
|
||||
{ value: 'failed', label: '投递失败' },
|
||||
{ value: 'duplicate', label: '重复投递' }
|
||||
]},
|
||||
{ title: '反馈状态', key: 'feedbackStatus', type: 'select', options: [
|
||||
{ value: 'none', label: '无反馈' },
|
||||
{ value: 'viewed', label: '已查看' },
|
||||
{ value: 'interested', label: '感兴趣' },
|
||||
{ value: 'not_suitable', label: '不合适' },
|
||||
{ value: 'interview', label: '面试邀约' }
|
||||
]},
|
||||
{ title: 'HR姓名', key: 'hrName', type: 'text' },
|
||||
{ title: 'HR职位', key: 'hrPosition', type: 'text' },
|
||||
{ title: '匹配度', key: 'matchScore', type: 'number' },
|
||||
{ title: '是否外包', key: 'isOutsourcing', type: 'switch' },
|
||||
{ title: '备注', key: 'notes', type: 'textarea' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
applyRecordsServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await applyRecordsServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
applyRecordsServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '投递记录.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'jobTitle',
|
||||
value: '',
|
||||
platform: null,
|
||||
applyStatus: null,
|
||||
feedbackStatus: null
|
||||
}
|
||||
this.gridOption.param.pageOption.page = 1
|
||||
this.query(1)
|
||||
},
|
||||
showApplyDetail(row) {
|
||||
this.$Modal.info({
|
||||
title: '投递记录详情',
|
||||
width: 800,
|
||||
render: (h) => {
|
||||
return h('div', { style: { maxHeight: '500px', overflowY: 'auto' } }, [
|
||||
h('h3', { style: { marginBottom: '15px', color: '#2d8cf0' } }, '基本信息'),
|
||||
h('p', [h('strong', '设备SN码: '), row.sn_code || '未知']),
|
||||
h('p', [h('strong', '平台: '), row.platform === 'boss' ? 'Boss直聘' : row.platform === 'liepin' ? '猎聘' : row.platform]),
|
||||
h('p', [h('strong', '岗位名称: '), row.jobTitle]),
|
||||
h('p', [h('strong', '公司名称: '), row.companyName]),
|
||||
h('p', [h('strong', '薪资: '), row.salary || '未知']),
|
||||
h('p', [h('strong', '地点: '), row.location || '未知']),
|
||||
|
||||
h('h3', { style: { marginTop: '20px', marginBottom: '15px', color: '#2d8cf0' } }, '投递状态'),
|
||||
h('p', [h('strong', '投递状态: '), this.getApplyStatusText(row.applyStatus)]),
|
||||
h('p', [h('strong', '反馈状态: '), this.getFeedbackStatusText(row.feedbackStatus)]),
|
||||
h('p', [h('strong', '投递时间: '), row.applyTime || '未知']),
|
||||
h('p', [h('strong', '匹配度: '), `${row.matchScore || 0}%`]),
|
||||
|
||||
h('h3', { style: { marginTop: '20px', marginBottom: '15px', color: '#2d8cf0' } }, 'HR信息'),
|
||||
h('p', [h('strong', 'HR姓名: '), row.hrName || '未知']),
|
||||
h('p', [h('strong', 'HR职位: '), row.hrPosition || '未知']),
|
||||
|
||||
h('h3', { style: { marginTop: '20px', marginBottom: '15px', color: '#2d8cf0' } }, '其他信息'),
|
||||
h('p', [h('strong', '是否外包: '), row.isOutsourcing ? '是' : '否']),
|
||||
row.jobUrl ? h('p', [
|
||||
h('strong', '岗位链接: '),
|
||||
h('a', { attrs: { href: row.jobUrl, target: '_blank' } }, row.jobUrl)
|
||||
]) : null,
|
||||
row.notes ? h('div', { style: { marginTop: '15px' } }, [
|
||||
h('strong', '备注:'),
|
||||
h('p', { style: { whiteSpace: 'pre-wrap', marginTop: '10px', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px' } }, row.notes)
|
||||
]) : null
|
||||
])
|
||||
}
|
||||
})
|
||||
},
|
||||
getApplyStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '待投递',
|
||||
'applying': '投递中',
|
||||
'success': '投递成功',
|
||||
'failed': '投递失败',
|
||||
'duplicate': '重复投递'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
},
|
||||
getFeedbackStatusText(status) {
|
||||
const statusMap = {
|
||||
'none': '无反馈',
|
||||
'viewed': '已查看',
|
||||
'interested': '感兴趣',
|
||||
'not_suitable': '不合适',
|
||||
'interview': '面试邀约'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
317
admin/src/views/work/job_postings.vue
Normal file
317
admin/src/views/work/job_postings.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增岗位</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="平台">
|
||||
<Select v-model="gridOption.param.seachOption.platform" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="boss">Boss直聘</Option>
|
||||
<Option value="liepin">猎聘</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="是否外包">
|
||||
<Select v-model="gridOption.param.seachOption.isOutsourcing" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option :value="true">是</Option>
|
||||
<Option :value="false">否</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
<Button type="default" @click="exportCsv">导出</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jobPostingsServer from '@/api/work/job_postings_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["jobTitle"] = [{ required: true, message: '请填写岗位名称', trigger: 'blur' }]
|
||||
rules["companyName"] = [{ required: true, message: '请填写公司名称', trigger: 'blur' }]
|
||||
rules["platform"] = [{ required: true, message: '请选择平台', trigger: 'change' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'jobTitle', value: '岗位名称' },
|
||||
{ key: 'companyName', value: '公司名称' },
|
||||
{ key: 'jobId', value: '岗位ID' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'jobTitle',
|
||||
value: '',
|
||||
platform: null,
|
||||
isOutsourcing: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: '岗位ID', key: 'jobId', minWidth: 180 },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const platformMap = {
|
||||
'boss': { text: 'Boss直聘', color: 'blue' },
|
||||
'liepin': { text: '猎聘', color: 'green' }
|
||||
}
|
||||
const platform = platformMap[params.row.platform] || { text: params.row.platform, color: 'default' }
|
||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||
}
|
||||
},
|
||||
{ title: '岗位名称', key: 'jobTitle', minWidth: 180 },
|
||||
{ title: '公司名称', key: 'companyName', minWidth: 180 },
|
||||
{ title: '薪资', key: 'salary', minWidth: 120 },
|
||||
{ title: '地点', key: 'location', minWidth: 120 },
|
||||
{ title: '经验要求', key: 'experience', minWidth: 100 },
|
||||
{ title: '学历要求', key: 'education', minWidth: 100 },
|
||||
{
|
||||
title: 'AI匹配度',
|
||||
key: 'aiMatchScore',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const score = params.row.aiMatchScore || 0
|
||||
const color = score >= 80 ? 'success' : score >= 60 ? 'warning' : 'error'
|
||||
return h('Tag', { props: { color: color } }, `${score}%`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '是否外包',
|
||||
key: 'isOutsourcing',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.isOutsourcing ? 'warning' : 'success' }
|
||||
}, params.row.isOutsourcing ? '是' : '否')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '投递状态',
|
||||
key: 'applyStatus',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const statusMap = {
|
||||
'pending': { text: '待投递', color: 'default' },
|
||||
'applied': { text: '已投递', color: 'success' },
|
||||
'rejected': { text: '被拒绝', color: 'error' },
|
||||
'accepted': { text: '已接受', color: 'blue' }
|
||||
}
|
||||
const status = statusMap[params.row.applyStatus] || statusMap['pending']
|
||||
return h('Tag', { props: { color: status.color } }, status.text)
|
||||
}
|
||||
},
|
||||
{ title: '发布时间', key: 'publishTime', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 300,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
const isApplied = params.row.applyStatus === 'applied'
|
||||
let btns = [
|
||||
{
|
||||
title: '查看详情',
|
||||
type: 'info',
|
||||
click: () => {
|
||||
this.showJobDetail(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: isApplied ? '已投递' : '打招呼',
|
||||
type: isApplied ? 'default' : 'success',
|
||||
disabled: isApplied,
|
||||
click: () => {
|
||||
if (!isApplied) {
|
||||
this.jobGreet(params.row)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '岗位ID', key: 'jobId', type: 'text', required: false },
|
||||
{ title: '平台', key: 'platform', type: 'select', required: true, options: [
|
||||
{ value: 'boss', label: 'Boss直聘' },
|
||||
{ value: 'liepin', label: '猎聘' }
|
||||
]},
|
||||
{ title: '岗位名称', key: 'jobTitle', type: 'text', required: true },
|
||||
{ title: '公司名称', key: 'companyName', type: 'text', required: true },
|
||||
{ title: '薪资', key: 'salary', type: 'text' },
|
||||
{ title: '地点', key: 'location', type: 'text' },
|
||||
{ title: '经验要求', key: 'experience', type: 'text' },
|
||||
{ title: '学历要求', key: 'education', type: 'text' },
|
||||
{ title: '岗位描述', key: 'jobDescription', type: 'textarea' },
|
||||
{ title: '岗位链接', key: 'jobUrl', type: 'text' },
|
||||
{ title: 'AI匹配度', key: 'aiMatchScore', type: 'number' },
|
||||
{ title: '技能匹配度', key: 'aiSkillMatch', type: 'number' },
|
||||
{ title: '经验匹配度', key: 'aiExperienceMatch', type: 'number' },
|
||||
{ title: '薪资合理性', key: 'aiSalaryReasonable', type: 'number' },
|
||||
{ title: '公司质量评分', key: 'aiCompanyQuality', type: 'number' },
|
||||
{ title: 'AI分析结果', key: 'aiAnalysisResult', type: 'textarea' },
|
||||
{ title: '是否外包', key: 'isOutsourcing', type: 'switch' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
jobPostingsServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await jobPostingsServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
exportCsv() {
|
||||
jobPostingsServer.exportCsv(this.gridOption.param).then(res => {
|
||||
window.framework.funTool.downloadFile(res, '岗位信息.csv')
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'jobTitle',
|
||||
value: '',
|
||||
platform: null,
|
||||
isOutsourcing: null
|
||||
}
|
||||
this.query(1)
|
||||
},
|
||||
showJobDetail(row) {
|
||||
this.$Modal.info({
|
||||
title: '岗位详情',
|
||||
width: 800,
|
||||
render: (h) => {
|
||||
return h('div', { style: { maxHeight: '500px', overflowY: 'auto' } }, [
|
||||
h('p', [h('strong', '岗位ID: '), row.jobId]),
|
||||
h('p', [h('strong', '平台: '), row.platform === 'boss' ? 'Boss直聘' : row.platform]),
|
||||
h('p', [h('strong', '岗位名称: '), row.jobTitle]),
|
||||
h('p', [h('strong', '公司名称: '), row.companyName]),
|
||||
h('p', [h('strong', '薪资: '), row.salary || '未知']),
|
||||
h('p', [h('strong', '地点: '), row.location || '未知']),
|
||||
h('p', [h('strong', '经验要求: '), row.experience || '未知']),
|
||||
h('p', [h('strong', '学历要求: '), row.education || '未知']),
|
||||
h('p', [h('strong', 'AI匹配度: '), `${row.aiMatchScore || 0}%`]),
|
||||
h('p', [h('strong', '是否外包: '), row.isOutsourcing ? '是' : '否']),
|
||||
h('p', [h('strong', '发布时间: '), row.publishTime || '未知']),
|
||||
row.jobDescription ? h('div', [
|
||||
h('strong', '岗位描述:'),
|
||||
h('p', { style: { whiteSpace: 'pre-wrap', marginTop: '10px' } }, row.jobDescription)
|
||||
]) : null,
|
||||
row.jobUrl ? h('p', [
|
||||
h('strong', '岗位链接: '),
|
||||
h('a', { attrs: { href: row.jobUrl, target: '_blank' } }, row.jobUrl)
|
||||
]) : null
|
||||
])
|
||||
}
|
||||
})
|
||||
},
|
||||
jobGreet(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认打招呼',
|
||||
content: `确定要向 ${row.companyName} 的 ${row.jobTitle} 岗位打招呼吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const loading = this.$Message.loading({
|
||||
content: '正在发送打招呼...',
|
||||
duration: 0
|
||||
})
|
||||
|
||||
// 获取设备SN码(这里需要从配置或用户选择中获取)
|
||||
const sn_code = localStorage.getItem('current_sn_code') || 'GHJU'
|
||||
|
||||
await jobPostingsServer.jobGreet({
|
||||
sn_code: sn_code,
|
||||
encryptJobId: row.jobId,
|
||||
brandName: row.companyName,
|
||||
platform: row.platform || 'boss'
|
||||
})
|
||||
|
||||
loading()
|
||||
this.$Message.success('打招呼成功!')
|
||||
|
||||
// 刷新列表
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} catch (error) {
|
||||
this.$Message.error('打招呼失败:' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
331
admin/src/views/work/job_types.vue
Normal file
331
admin/src/views/work/job_types.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增职位类型</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select v-model="gridOption.param.seachOption.is_enabled" style="width: 120px" clearable
|
||||
@on-change="query(1)">
|
||||
<Option :value="1">启用</Option>
|
||||
<Option :value="0">禁用</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules" @on-save="handleSaveSuccess">
|
||||
</editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jobTypesServer from '@/api/work/job_types_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["name"] = [{ required: true, message: '请填写职位类型名称', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'name', value: '职位类型名称' },
|
||||
{ key: 'description', value: '描述' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'name',
|
||||
value: '',
|
||||
is_enabled: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '职位类型名称', key: 'name', minWidth: 150 },
|
||||
{ title: '描述', key: 'description', minWidth: 200 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'is_enabled',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const status = params.row.is_enabled === 1
|
||||
return h('Tag', { props: { color: status ? 'success' : 'default' } }, status ? '启用' : '禁用')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '常见技能关键词', key: 'commonSkills', minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '排除关键词', key: 'excludeKeywords', minWidth: 200
|
||||
},
|
||||
{ title: '排序', key: 'sort_order', minWidth: 80 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
render: (h, params) => {
|
||||
return h('div', [
|
||||
h('Button', {
|
||||
props: {
|
||||
type: 'primary',
|
||||
size: 'small'
|
||||
},
|
||||
style: {
|
||||
marginRight: '5px'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.edit(params.row)
|
||||
}
|
||||
}
|
||||
}, '编辑'),
|
||||
h('Button', {
|
||||
props: {
|
||||
type: 'error',
|
||||
size: 'small'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.del(params.row)
|
||||
}
|
||||
}
|
||||
}, '删除')
|
||||
])
|
||||
}
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{
|
||||
title: '职位类型名称',
|
||||
key: 'name',
|
||||
type: 'input',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
com: 'TextArea',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
title: '常见技能关键词',
|
||||
key: 'commonSkills',
|
||||
com: 'TextArea',
|
||||
|
||||
required: false,
|
||||
placeholder: '请输入JSON数组格式,例如:["Vue", "React", "Node.js"]',
|
||||
tooltip: '技能关键词列表,JSON数组格式'
|
||||
},
|
||||
{
|
||||
title: '排除关键词',
|
||||
key: 'excludeKeywords',
|
||||
com: 'TextArea',
|
||||
required: false,
|
||||
placeholder: '请输入JSON数组格式,例如:["外包", "销售", "客服"]',
|
||||
tooltip: '排除关键词列表,JSON数组格式'
|
||||
},
|
||||
{
|
||||
title: '是否启用',
|
||||
key: 'is_enabled',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 1, label: '启用' },
|
||||
{ value: 0, label: '禁用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '排序顺序',
|
||||
key: 'sort_order',
|
||||
type: 'number',
|
||||
required: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const item = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return item ? `请输入${item.value}` : '请选择'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
if (page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
}
|
||||
const param = {
|
||||
pageOption: this.gridOption.param.pageOption,
|
||||
seachOption: {}
|
||||
}
|
||||
if (this.gridOption.param.seachOption.key && this.gridOption.param.seachOption.value) {
|
||||
param.seachOption[this.gridOption.param.seachOption.key] = this.gridOption.param.seachOption.value
|
||||
}
|
||||
if (this.gridOption.param.seachOption.is_enabled !== null) {
|
||||
param.seachOption.is_enabled = this.gridOption.param.seachOption.is_enabled
|
||||
}
|
||||
jobTypesServer.page(param).then(res => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data
|
||||
this.gridOption.data = data.rows
|
||||
this.gridOption.param.pageOption.total = data.count || data.total || 0
|
||||
} else {
|
||||
this.$Message.error(res.message || '查询失败')
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error('查询失败:' + (err.message || '未知错误'))
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'name',
|
||||
value: '',
|
||||
is_enabled: null
|
||||
}
|
||||
this.query(1)
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.show({
|
||||
name: '',
|
||||
description: '',
|
||||
commonSkills: '[]',
|
||||
excludeKeywords: '[]',
|
||||
is_enabled: 1,
|
||||
sort_order: 0
|
||||
})
|
||||
},
|
||||
edit(row) {
|
||||
// 解析 JSON 字段
|
||||
let commonSkills = row.commonSkills || '[]'
|
||||
let excludeKeywords = row.excludeKeywords || '[]'
|
||||
|
||||
// 如果是字符串,尝试解析并格式化
|
||||
if (typeof commonSkills === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(commonSkills)
|
||||
commonSkills = JSON.stringify(parsed, null, 2)
|
||||
} catch (e) {
|
||||
// 保持原样
|
||||
}
|
||||
} else {
|
||||
commonSkills = JSON.stringify(commonSkills, null, 2)
|
||||
}
|
||||
|
||||
if (typeof excludeKeywords === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(excludeKeywords)
|
||||
excludeKeywords = JSON.stringify(parsed, null, 2)
|
||||
} catch (e) {
|
||||
// 保持原样
|
||||
}
|
||||
} else {
|
||||
excludeKeywords = JSON.stringify(excludeKeywords, null, 2)
|
||||
}
|
||||
|
||||
this.$refs.editModal.editShow({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description || '',
|
||||
commonSkills: commonSkills,
|
||||
excludeKeywords: excludeKeywords,
|
||||
is_enabled: row.is_enabled,
|
||||
sort_order: row.sort_order || 0
|
||||
})
|
||||
},
|
||||
del(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除职位类型"${row.name}"吗?`,
|
||||
onOk: () => {
|
||||
jobTypesServer.del(row).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$Message.success('删除成功')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || '删除失败')
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error('删除失败:' + (err.message || '未知错误'))
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleSaveSuccess(data) {
|
||||
// 处理 JSON 字段
|
||||
const formData = { ...data }
|
||||
|
||||
// 处理 commonSkills
|
||||
if (formData.commonSkills) {
|
||||
try {
|
||||
const parsed = typeof formData.commonSkills === 'string'
|
||||
? JSON.parse(formData.commonSkills)
|
||||
: formData.commonSkills
|
||||
formData.commonSkills = Array.isArray(parsed) ? parsed : []
|
||||
} catch (e) {
|
||||
this.$Message.warning('常见技能关键词格式错误,将使用空数组')
|
||||
formData.commonSkills = []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 excludeKeywords
|
||||
if (formData.excludeKeywords) {
|
||||
try {
|
||||
const parsed = typeof formData.excludeKeywords === 'string'
|
||||
? JSON.parse(formData.excludeKeywords)
|
||||
: formData.excludeKeywords
|
||||
formData.excludeKeywords = Array.isArray(parsed) ? parsed : []
|
||||
} catch (e) {
|
||||
this.$Message.warning('排除关键词格式错误,将使用空数组')
|
||||
formData.excludeKeywords = []
|
||||
}
|
||||
}
|
||||
|
||||
const apiMethod = formData.id ? jobTypesServer.update : jobTypesServer.add
|
||||
apiMethod(formData).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$Message.success(formData.id ? '更新成功' : '添加成功')
|
||||
this.$refs.editModal.hide()
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || (formData.id ? '更新失败' : '添加失败'))
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error((formData.id ? '更新失败' : '添加失败') + ':' + (err.message || '未知错误'))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-view {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user