265 lines
9.6 KiB
JavaScript
265 lines
9.6 KiB
JavaScript
// pages/match-job/index.js - 匹配工作(简历 + 性格 + AI 综合分析)
|
||
const app = getApp()
|
||
const { request } = require('../../utils/request')
|
||
|
||
const DEFAULT_MBTI_JOB = {
|
||
ISTJ: '适合财务、审计、行政、项目经理等结构化岗位。',
|
||
ISFJ: '护理、教师、客户服务、HR 等以关怀为主的岗位。',
|
||
INFJ: '心理咨询、人力资源、作家、战略策划等职业。',
|
||
INTJ: '战略顾问、架构师、产品总监、科研等。',
|
||
ISTP: '工程师、技术员、运维、维修、运动相关。',
|
||
ISFP: '设计师、艺术家、摄影师、UX 等。',
|
||
INFP: '作家、心理咨询、社工、内容策划等。',
|
||
INTP: '程序员、研究员、数据分析师、系统架构师。',
|
||
ESTP: '销售、创业、运动员、媒体、现场管理。',
|
||
ESFP: '演员、销售、主持、公关、活动策划。',
|
||
ENFP: '市场营销、新媒体、咨询、品牌。',
|
||
ENTP: '律师、企业家、创投、产品、战略咨询。',
|
||
ESTJ: '管理者、项目经理、运营总监、律师。',
|
||
ESFJ: '教师、护士、HR、运营管理、社区。',
|
||
ENFJ: '培训师、HR、教师、销售总监、咨询。',
|
||
ENTJ: 'CEO、创业者、咨询合伙人、业务负责人。'
|
||
}
|
||
const DEFAULT_PDP_JOB = {
|
||
Tiger: '老虎型适合担任决策者/领导者岗位:CEO、业务负责人、销售总监。',
|
||
Peacock: '孔雀型适合表达/影响岗位:市场、公关、培训、销售、主播。',
|
||
Koala: '无尾熊型适合支持/稳定岗位:HR、客户服务、行政、项目协作。',
|
||
Owl: '猫头鹰型适合分析/精准岗位:数据分析、研发、审计、质量管理。',
|
||
Chameleon: '变色龙型适合协调/多面手岗位:顾问、项目经理、咨询、创业。'
|
||
}
|
||
const DEFAULT_DISC_JOB = {
|
||
D: 'D 型适合业务开拓/领导类岗位:销售、创业、管理。',
|
||
I: 'I 型适合表达影响类岗位:市场、公关、培训、社群。',
|
||
S: 'S 型适合服务支持类岗位:HR、客服、运营、项目协作。',
|
||
C: 'C 型适合分析严谨类岗位:研发、数据、财务、质量控制。'
|
||
}
|
||
|
||
Page({
|
||
data: {
|
||
mbtiType: '',
|
||
pdpType: '',
|
||
discType: '',
|
||
sbtiType: '',
|
||
faceType: '',
|
||
mbtiJobHint: '',
|
||
pdpJobHint: '',
|
||
discJobHint: '',
|
||
hasAnyTest: false,
|
||
resumeUrl: '',
|
||
resumeFileName: '',
|
||
uploading: false,
|
||
analyzing: false,
|
||
analyzed: false,
|
||
analyzeResult: null
|
||
},
|
||
|
||
onLoad() {
|
||
try { require('../../utils/analytics').track('page_view', { path: 'pages/match-job/index' }) } catch (e) {}
|
||
app.ensureLogin && app.ensureLogin().then(() => {
|
||
this.loadPersonalityTags()
|
||
this.loadDefaultResume()
|
||
})
|
||
},
|
||
|
||
onShow() {
|
||
this.loadPersonalityTags()
|
||
},
|
||
|
||
loadPersonalityTags() {
|
||
request({
|
||
url: '/api/test/recent',
|
||
method: 'GET',
|
||
needAuth: true,
|
||
success: (res) => {
|
||
const d = (res && res.data && res.data.code === 200 && res.data.data) || {}
|
||
const mbti = this._pickType(d.mbti, 'mbti')
|
||
const pdp = this._pickType(d.pdp, 'pdp')
|
||
const disc = this._pickType(d.disc, 'disc')
|
||
const sbti = this._pickType(d.sbti, 'sbti')
|
||
const face = this._pickType(d.face, 'face')
|
||
const hasAnyTest = !!(mbti || pdp || disc || sbti || face)
|
||
this.setData({
|
||
mbtiType: mbti,
|
||
pdpType: pdp,
|
||
discType: disc,
|
||
sbtiType: sbti,
|
||
faceType: face,
|
||
hasAnyTest,
|
||
mbtiJobHint: mbti ? (DEFAULT_MBTI_JOB[mbti] || '结合所属类别的典型岗位方向。') : '',
|
||
pdpJobHint: pdp ? (DEFAULT_PDP_JOB[pdp] || '') : '',
|
||
discJobHint: disc ? (DEFAULT_DISC_JOB[(disc || '').charAt(0).toUpperCase()] || '') : ''
|
||
})
|
||
},
|
||
fail: () => {}
|
||
})
|
||
},
|
||
|
||
_pickType(rec, type) {
|
||
if (!rec) return ''
|
||
try {
|
||
const rd = typeof rec.resultData === 'string' ? JSON.parse(rec.resultData) : (rec.resultData || {})
|
||
if (type === 'mbti') return rd.mbtiType || rd.mbti || ''
|
||
if (type === 'pdp') return (rd.description && rd.description.type) || rd.dominantType || ''
|
||
if (type === 'disc') return rd.dominantType || rd.disc || ''
|
||
if (type === 'sbti') return rd.sbtiType || (rd.finalType && rd.finalType.code) || ''
|
||
if (type === 'face') return rd.mbti || rd.faceType || ''
|
||
} catch (e) {}
|
||
return ''
|
||
},
|
||
|
||
loadDefaultResume() {
|
||
// 取默认简历(若有)
|
||
request({
|
||
url: '/api/enterprise/resume-uploads?pageSize=1',
|
||
method: 'GET',
|
||
needAuth: true,
|
||
success: (res) => {
|
||
const list = (res && res.data && res.data.code === 200 && res.data.data && res.data.data.list) || []
|
||
const def = list.find((r) => r.isDefault) || list[0]
|
||
if (def) {
|
||
this.setData({ resumeUrl: def.url || '', resumeFileName: def.fileName || '' })
|
||
}
|
||
},
|
||
fail: () => {}
|
||
})
|
||
},
|
||
|
||
chooseResume() {
|
||
try { require('../../utils/analytics').track('tap_match_upload_resume', {}) } catch (e) {}
|
||
wx.chooseMessageFile({
|
||
count: 1,
|
||
type: 'file',
|
||
extension: ['jpg', 'jpeg', 'png', 'webp', 'pdf', 'doc', 'docx'],
|
||
success: (fileRes) => {
|
||
const files = fileRes.tempFiles || []
|
||
if (!files.length) return
|
||
const file = files[0]
|
||
const filePath = file.path || file.tempFilePath
|
||
const fileName = file.name || '简历文件'
|
||
this._doUpload(filePath, fileName)
|
||
}
|
||
})
|
||
},
|
||
|
||
_doUpload(filePath, fileName) {
|
||
this.setData({ uploading: true })
|
||
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
|
||
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
|
||
wx.showLoading({ title: '上传中...', mask: true })
|
||
wx.uploadFile({
|
||
url: apiBase + '/api/upload/file',
|
||
filePath,
|
||
name: 'file',
|
||
header: token ? { Authorization: 'Bearer ' + token } : {},
|
||
success: (res) => {
|
||
wx.hideLoading()
|
||
try {
|
||
const data = JSON.parse(res.data)
|
||
if (data.code === 200 && data.data && data.data.url) {
|
||
const url = data.data.url
|
||
// 记录一条简历上传(方便历史页看)
|
||
request({
|
||
url: '/api/enterprise/resume-uploads',
|
||
method: 'POST',
|
||
needAuth: true,
|
||
data: { url, fileName },
|
||
success: () => {},
|
||
fail: () => {}
|
||
})
|
||
this.setData({ resumeUrl: url, resumeFileName: fileName })
|
||
wx.showToast({ title: '上传成功', icon: 'success' })
|
||
} else {
|
||
wx.showToast({ title: data.message || '上传失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
wx.showToast({ title: '解析上传结果失败', icon: 'none' })
|
||
}
|
||
},
|
||
fail: () => {
|
||
wx.hideLoading()
|
||
wx.showToast({ title: '上传失败', icon: 'none' })
|
||
},
|
||
complete: () => this.setData({ uploading: false })
|
||
})
|
||
},
|
||
|
||
previewResume() {
|
||
const url = this.data.resumeUrl
|
||
if (!url) return
|
||
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
|
||
const full = url.startsWith('http') ? url : apiBase + url
|
||
wx.downloadFile({
|
||
url: full,
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.tempFilePath) {
|
||
wx.openDocument({ filePath: res.tempFilePath, showMenu: true })
|
||
} else {
|
||
wx.showToast({ title: '打开失败', icon: 'none' })
|
||
}
|
||
},
|
||
fail: () => wx.showToast({ title: '打开失败', icon: 'none' })
|
||
})
|
||
},
|
||
|
||
runAnalyze() {
|
||
if (!this.data.hasAnyTest) {
|
||
wx.showToast({ title: '请先完成至少一项性格测评', icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.data.resumeUrl) {
|
||
wx.showToast({ title: '请先上传简历', icon: 'none' })
|
||
return
|
||
}
|
||
this.setData({ analyzing: true })
|
||
try { require('../../utils/analytics').track('tap_match_analyze', { mbti: this.data.mbtiType, pdp: this.data.pdpType }) } catch (e) {}
|
||
wx.showLoading({ title: 'AI 分析中...', mask: true })
|
||
request({
|
||
url: '/api/resume/analyze',
|
||
method: 'POST',
|
||
needAuth: true,
|
||
data: { resumeUrl: this.data.resumeUrl },
|
||
success: (res) => {
|
||
wx.hideLoading()
|
||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||
const d = res.data.data || {}
|
||
this.setData({
|
||
analyzeResult: this._normalizeAnalyzeResult(d),
|
||
analyzed: true
|
||
})
|
||
} else {
|
||
wx.showToast({ title: (res.data && res.data.message) || '分析失败', icon: 'none' })
|
||
}
|
||
},
|
||
fail: () => {
|
||
wx.hideLoading()
|
||
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
|
||
},
|
||
complete: () => this.setData({ analyzing: false })
|
||
})
|
||
},
|
||
|
||
_normalizeAnalyzeResult(d) {
|
||
const fit = Array.isArray(d.fitRoles)
|
||
? d.fitRoles
|
||
: (d.portrait && Array.isArray(d.portrait.bestFit) ? d.portrait.bestFit : [])
|
||
const strengths = Array.isArray(d.strengths)
|
||
? d.strengths
|
||
: (d.portrait && Array.isArray(d.portrait.coreStrengths) ? d.portrait.coreStrengths : [])
|
||
const weaknesses = Array.isArray(d.weaknesses)
|
||
? d.weaknesses
|
||
: (d.portrait && Array.isArray(d.portrait.coreRisks) ? d.portrait.coreRisks : [])
|
||
return {
|
||
score: typeof d.score === 'number' ? d.score : (d.matchScore || null),
|
||
fitRoles: fit.slice(0, 5),
|
||
summary: d.summary || (d.portrait && d.portrait.workStyle) || '',
|
||
strengths: strengths.slice(0, 8),
|
||
weaknesses: weaknesses.slice(0, 6),
|
||
advice: d.advice || d.nextStep || ''
|
||
}
|
||
},
|
||
|
||
goToTestSelect() {
|
||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||
}
|
||
})
|