This commit is contained in:
张成
2025-11-24 13:23:42 +08:00
commit 5d7444cd65
156 changed files with 50653 additions and 0 deletions

130
admin/src/api/README.md Normal file
View 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 菜单定义保持一致

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

File diff suppressed because one or more lines are too long

27
admin/src/main.js Normal file
View 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 已在文件开头暴露,无需重复

View 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
View 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

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

View 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>

View 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. 更新此文档说明
## 📄 许可
此组件为项目内部组件,仅供项目内使用。

View 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>

View 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>

View 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>

View 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>