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

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>