index.vue 78 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243
  1. <script setup lang="ts">
  2. import { ref, onMounted } from 'vue'
  3. import { message } from 'ant-design-vue'
  4. import type { FormInstance } from 'ant-design-vue'
  5. import { useRoute } from 'vue-router'
  6. import { ElMessage } from 'element-plus'
  7. import { Share, Download, Printer, ArrowUp, User } from '@element-plus/icons-vue'
  8. interface CandidateInfo {
  9. name: string
  10. idNumber: string
  11. phoneNumber: string
  12. score: number
  13. availabilityPeriod: string
  14. onboardingTime: string
  15. suggestedSalary: string
  16. salaryRange:string
  17. dimensions: {
  18. teamwork: string
  19. learningAbility: string
  20. attention: string
  21. workAdaptability: string
  22. serviceAwareness: string
  23. }
  24. dimensionDetails: {
  25. teamwork: string
  26. learningAbility: string
  27. attention: string
  28. workAdaptability: string
  29. serviceAwareness: string
  30. }
  31. duvAnalysis: {
  32. title: string
  33. content: string
  34. score: string
  35. type: 'positive' | 'negative' | 'neutral'
  36. }[]
  37. interviewRecord: {
  38. question: string
  39. answer: string
  40. analysis: string
  41. score: string
  42. videoUrl?: string
  43. thumbnail?: string
  44. question_form?: number
  45. is_correct?: boolean | string
  46. question_image_url?: string
  47. options?: {
  48. option_text: string
  49. is_correct: boolean
  50. }[]
  51. }[]
  52. videoRecords: {
  53. category: string
  54. videos: {
  55. url: string
  56. thumbnail: string
  57. description: string
  58. }[]
  59. }[]
  60. additionalInfo: {
  61. idVerification: string
  62. phoneVerification: string
  63. leftHandGestures: {
  64. passed: boolean
  65. images: string[]
  66. }
  67. rightHandGestures: {
  68. passed: boolean
  69. images: string[]
  70. }
  71. faceVerification: {
  72. passed: boolean
  73. images: string[]
  74. }
  75. comprehensiveVerification: string
  76. handVerification: string
  77. handBrightness: string
  78. handIntegrity: string
  79. handPoseRecognition: string
  80. }
  81. strengths?: string[]
  82. weaknesses?: string[]
  83. hireRecommendation?: string
  84. hireReason?: string
  85. }
  86. interface UserProfile {
  87. user_info: {
  88. id: number;
  89. name: string;
  90. phone: string;
  91. age: number;
  92. birth_date: string;
  93. gender: number;
  94. gender_name: string;
  95. professional_skills: string | null;
  96. id_card: string;
  97. status: number;
  98. examine: string;
  99. source: string;
  100. create_datetime: string;
  101. update_datetime: string;
  102. };
  103. profile: {
  104. political_status: string;
  105. ethnicity: string;
  106. height: string;
  107. weight: string;
  108. native_place: string;
  109. household_location: string;
  110. current_address: string;
  111. female_status: number;
  112. female_status_name: string;
  113. marital_status: number;
  114. marital_status_name: string;
  115. has_children: boolean;
  116. expected_salary: string;
  117. emergency_contact: string;
  118. emergency_phone: string;
  119. specialties: string;
  120. life_motto: string;
  121. recruitment_source: number;
  122. recruitment_source_name: string;
  123. recruitment_source_detail: string;
  124. };
  125. educations: Array<{
  126. id: number;
  127. education_type: number;
  128. education_type_name: string;
  129. degree: number;
  130. degree_name: string;
  131. start_date: string;
  132. end_date: string;
  133. school_name: string;
  134. major: string;
  135. create_datetime: string;
  136. update_datetime: string;
  137. }>;
  138. family_members: Array<{
  139. id: number;
  140. relation: string;
  141. name: string;
  142. workplace: string;
  143. position: string;
  144. phone: string;
  145. create_datetime: string;
  146. update_datetime: string;
  147. }>;
  148. trainings: Array<{
  149. id: number;
  150. training_name: string;
  151. institution: string;
  152. start_date: string;
  153. end_date: string;
  154. description: string;
  155. certificate: string;
  156. create_datetime: string;
  157. update_datetime: string;
  158. }>;
  159. work_experiences: Array<{
  160. id: number;
  161. company_name: string;
  162. start_date: string;
  163. end_date: string;
  164. department: string;
  165. position: string;
  166. company_size: number;
  167. monthly_salary: string;
  168. supervisor_name: string;
  169. supervisor_phone: string;
  170. job_description: string | null;
  171. achievements: string | null;
  172. resignation_reason: string | null;
  173. create_datetime: string;
  174. update_datetime: string;
  175. }>;
  176. }
  177. const candidateInfo = ref<CandidateInfo>({
  178. name: '',
  179. idNumber: '',
  180. phoneNumber: '',
  181. score: 0,
  182. availabilityPeriod: '',
  183. onboardingTime: '',
  184. suggestedSalary: '',
  185. salaryRange: '',
  186. dimensions: {
  187. teamwork: '欠佳',
  188. learningAbility: '优秀',
  189. attention: '欠佳',
  190. workAdaptability: '优秀',
  191. serviceAwareness: '中等'
  192. },
  193. dimensionDetails: {
  194. teamwork: '',
  195. learningAbility: '',
  196. attention: '',
  197. workAdaptability: '',
  198. serviceAwareness: ''
  199. },
  200. duvAnalysis: [
  201. {
  202. title: '',
  203. content: '',
  204. score: '',
  205. type: 'neutral'
  206. },
  207. {
  208. title: '',
  209. content: '',
  210. score: '',
  211. type: 'neutral'
  212. }
  213. ],
  214. interviewRecord: [
  215. {
  216. question: '',
  217. answer: '',
  218. analysis: '',
  219. score: '',
  220. videoUrl: '',
  221. thumbnail: '',
  222. question_form: 0,
  223. is_correct: false,
  224. question_image_url: ''
  225. },
  226. {
  227. question: '',
  228. answer: '',
  229. analysis: '',
  230. score: '',
  231. videoUrl: '',
  232. thumbnail: '',
  233. question_form: 0,
  234. is_correct: false,
  235. question_image_url: ''
  236. }
  237. ],
  238. videoRecords: [
  239. {
  240. category: '服务态度测评',
  241. videos: [
  242. {
  243. url: '',
  244. thumbnail: '',
  245. description: ''
  246. },
  247. {
  248. url: '',
  249. thumbnail: '',
  250. description: ''
  251. }
  252. ]
  253. }
  254. ],
  255. additionalInfo: {
  256. idVerification: '通过',
  257. phoneVerification: '通过',
  258. leftHandGestures: {
  259. passed: true,
  260. images: [
  261. ]
  262. },
  263. rightHandGestures: {
  264. passed: true,
  265. images: [
  266. ]
  267. },
  268. faceVerification: {
  269. passed: true,
  270. images: [
  271. ]
  272. },
  273. comprehensiveVerification: '通过',
  274. handVerification: '通过',
  275. handBrightness: '通过',
  276. handIntegrity: '通过',
  277. handPoseRecognition: '通过'
  278. },
  279. strengths: [],
  280. weaknesses: [],
  281. hireRecommendation: '',
  282. hireReason: ''
  283. })
  284. const apiData = ref<{
  285. applicant?: any;
  286. application?: any;
  287. position?: any;
  288. interview_progress?: any[];
  289. posture_photos?: any[];
  290. scoring_summary?: {
  291. total_score_obtained: number;
  292. max_possible_score: number;
  293. answered_scoring_questions_count: number;
  294. scoring_details: any[];
  295. };
  296. required_questions_summary?: {
  297. all_correct: boolean;
  298. failed_questions_details: any[];
  299. };
  300. } | null>(null)
  301. const loading = ref<boolean>(true)
  302. const error = ref<string>('')
  303. const route = useRoute()
  304. const fetchApplicationDetail = async () => {
  305. loading.value = true
  306. try {
  307. // 从Vue Router获取参数
  308. const id = route.query.id as string || '1'
  309. const tenant_id = route.query.tenant_id as string || '1'
  310. const application_id = route.query.application_id as string || '1'
  311. console.log('Route params:', { id, tenant_id, application_id })
  312. // 使用正确的参数调用API
  313. const response = await fetch(`${import.meta.env.VITE_API_URL}/api/job/application_detail?tenant_id=${tenant_id}&application_id=${id || application_id}`)
  314. const result = await response.json()
  315. if (result.code === 2000) {
  316. apiData.value = result.data
  317. console.log(result.data)
  318. updateCandidateInfo(result.data)
  319. } else {
  320. error.value = result.msg || '获取数据失败'
  321. }
  322. } catch (err) {
  323. console.error('获取申请详情失败:', err)
  324. error.value = '获取数据失败,请稍后重试'
  325. } finally {
  326. loading.value = false
  327. }
  328. }
  329. const updateCandidateInfo = (data: any) => {
  330. if (!data) return
  331. const { applicant, application, position, interview_progress, posture_photos } = data
  332. // 更新基本信息
  333. candidateInfo.value.name = applicant?.name || candidateInfo.value.name
  334. candidateInfo.value.phoneNumber = applicant?.phone || candidateInfo.value.phoneNumber
  335. candidateInfo.value.idNumber = applicant?.id_card || candidateInfo.value.idNumber
  336. // 添加薪资相关信息处理
  337. if (position) {
  338. // 如果API返回了薪资范围,使用API数据
  339. if (position.salary_range) {
  340. candidateInfo.value.salaryRange = position.salary_range || '面议'
  341. /* candidateInfo.value.salaryRange.max = position.salary_range || '面议' */
  342. } else {
  343. // 否则使用默认值
  344. candidateInfo.value.salaryRange = '面议'
  345. }
  346. // 根据候选人评分计算建议薪资
  347. if (position.salary_range && typeof position.salary_range === 'string') {
  348. // 尝试从薪资范围字符串中提取最小值和最大值
  349. // 支持多种格式:'5000-8000'、'5,000-8,000'、'5000元-8000元'等
  350. let min = 0, max = 0;
  351. if (position.salary_range.includes('-')) {
  352. // 处理范围格式 (例如 "5000-8000")
  353. const parts = position.salary_range.split('-');
  354. min = parseInt(parts[0].trim().replace(/[^\d]/g, ''), 10);
  355. max = parseInt(parts[1].trim().replace(/[^\d]/g, ''), 10);
  356. } else if (position.salary_range.includes('~')) {
  357. // 处理波浪线格式 (例如 "5000~8000")
  358. const parts = position.salary_range.split('~');
  359. min = parseInt(parts[0].trim().replace(/[^\d]/g, ''), 10);
  360. max = parseInt(parts[1].trim().replace(/[^\d]/g, ''), 10);
  361. } else if (position.salary_range.includes('至')) {
  362. // 处理中文格式 (例如 "5000至8000")
  363. const parts = position.salary_range.split('至');
  364. min = parseInt(parts[0].trim().replace(/[^\d]/g, ''), 10);
  365. max = parseInt(parts[1].trim().replace(/[^\d]/g, ''), 10);
  366. }
  367. // 如果成功解析出最小值和最大值,则根据分数计算建议薪资
  368. if (!isNaN(min) && !isNaN(max) && min > 0 && max > 0 && max > min) {
  369. // 薪资计算逻辑
  370. // 1. 分数低于60分:给予最低薪资
  371. // 2. 分数在60-80分之间:给予中等薪资(最低薪资 + 薪资范围的1/3)
  372. // 3. 分数在80分以上:给予最高薪资
  373. console.log("score",application.comprehensive_score)
  374. const score = application.comprehensive_score;
  375. console.log(score)
  376. let suggested = min; // 默认给最低薪资
  377. if (score >= 80) {
  378. // 80分以上给最高薪资
  379. console.log(max)
  380. suggested = max;
  381. } else if (score >= 60) {
  382. // 60-80分给中等薪资(最低薪资 + 薪资范围的1/3)
  383. suggested = min + Math.floor((max - min) / 3);
  384. }else{
  385. // 低于60分保持最低薪资
  386. suggested = 0;
  387. }
  388. // 格式化薪资显示
  389. if (suggested >= 10000) {
  390. // 如果大于等于10000,显示为"x.x万"
  391. const inWan = (suggested / 10000).toFixed(1);
  392. candidateInfo.value.suggestedSalary = `${inWan}万`;
  393. } else {
  394. // 否则直接显示数字
  395. candidateInfo.value.suggestedSalary = `${suggested}`;
  396. }
  397. } else {
  398. // 如果无法解析薪资范围,则直接使用原始值
  399. candidateInfo.value.suggestedSalary = position.salary_range;
  400. }
  401. } else {
  402. // 如果没有薪资范围信息,设为面议
  403. candidateInfo.value.suggestedSalary = '面议';
  404. }
  405. } else {
  406. // 如果没有职位信息,设置默认值
  407. candidateInfo.value.salaryRange = '面议';
  408. candidateInfo.value.suggestedSalary = '面议';
  409. }
  410. // 更新综合分析
  411. if (application?.comprehensive_analysis) {
  412. // 更新综合评分
  413. if (application.comprehensive_analysis.comprehensive_score !== null &&
  414. application.comprehensive_analysis.comprehensive_score !== undefined) {
  415. candidateInfo.value.score = application.comprehensive_analysis.comprehensive_score
  416. }
  417. // 更新优缺点分析
  418. if (application.comprehensive_analysis.video_analysis_data) {
  419. const videoData = application.comprehensive_analysis.video_analysis_data
  420. // 更新优点
  421. if (videoData.strengths && videoData.strengths.length > 0) {
  422. candidateInfo.value.strengths = videoData.strengths.filter((s: string) =>
  423. s && !s.includes('无法从响应中提取')
  424. )
  425. }
  426. // 更新缺点
  427. if (videoData.weaknesses && videoData.weaknesses.length > 0) {
  428. candidateInfo.value.weaknesses = videoData.weaknesses.filter((w: string) =>
  429. w && !w.includes('无法从响应中提取')
  430. )
  431. }
  432. // 更新录用建议
  433. if (application.comprehensive_analysis.hire_recommendation) {
  434. candidateInfo.value.hireRecommendation = application.comprehensive_analysis.hire_recommendation
  435. }
  436. // 更新录用理由
  437. if (application.comprehensive_analysis.hire_reason) {
  438. candidateInfo.value.hireReason = application.comprehensive_analysis.hire_reason
  439. }
  440. }
  441. }
  442. // 更新能力维度评分
  443. if (application?.ai_capability_scores) {
  444. const dimensionMapping: Record<string, keyof typeof candidateInfo.value.dimensions> = {
  445. '专业性': 'workAdaptability',
  446. '沟通能力': 'teamwork',
  447. '技术匹配度': 'learningAbility',
  448. '解决问题能力': 'attention',
  449. '服务意识': 'serviceAwareness'
  450. }
  451. Object.entries(application.ai_capability_scores).forEach(([key, value]) => {
  452. const mappedKey = dimensionMapping[key]
  453. if (mappedKey && typeof value === 'number') {
  454. let rating = '中等'
  455. if (value >= 80) rating = '优秀'
  456. else if (value < 65) rating = '欠佳'
  457. candidateInfo.value.dimensions[mappedKey] = rating
  458. }
  459. })
  460. } else {
  461. // 如果没有AI能力评分,根据面试题目分析生成维度评分
  462. const dimensionScores: Record<string, number[]> = {
  463. teamwork: [],
  464. learningAbility: [],
  465. attention: [],
  466. workAdaptability: [],
  467. serviceAwareness: []
  468. }
  469. interview_progress?.forEach((q: any) => {
  470. if (q.video_answer && q.video_answer.ai_score) {
  471. // 根据问题类型和内容分配到不同维度
  472. if (q.question_text.includes('团队') || q.question_text.includes('合作')) {
  473. dimensionScores.teamwork.push(q.video_answer.ai_score)
  474. }
  475. if (q.question_text.includes('学习') || q.question_text.includes('技能')) {
  476. dimensionScores.learningAbility.push(q.video_answer.ai_score)
  477. }
  478. if (q.question_text.includes('细致') || q.question_text.includes('严谨') || q.question_text.includes('注意')) {
  479. dimensionScores.attention.push(q.video_answer.ai_score)
  480. }
  481. if (q.question_text.includes('适应') || q.question_text.includes('工作')) {
  482. dimensionScores.workAdaptability.push(q.video_answer.ai_score)
  483. }
  484. if (q.question_text.includes('服务') || q.question_text.includes('客户')) {
  485. dimensionScores.serviceAwareness.push(q.video_answer.ai_score)
  486. }
  487. }
  488. })
  489. // 计算每个维度的平均分并转换为评级
  490. Object.entries(dimensionScores).forEach(([key, scores]) => {
  491. if (scores.length > 0) {
  492. const avgScore = scores.reduce((sum, score) => sum + score, 0) / scores.length
  493. let rating = '中等'
  494. if (avgScore >= 80) rating = '优秀'
  495. else if (avgScore < 65) rating = '欠佳'
  496. candidateInfo.value.dimensions[key as keyof typeof candidateInfo.value.dimensions] = rating
  497. }
  498. })
  499. }
  500. // 更新维度详情
  501. const dimensionDetailsMapping: Record<string, string> = {
  502. teamwork: '候选人在团队协作方面的表现',
  503. learningAbility: '候选人的学习能力和接受新知识的速度',
  504. attention: '候选人对细节的关注程度和工作严谨性',
  505. workAdaptability: '候选人适应工作环境和要求的能力',
  506. serviceAwareness: '候选人的服务意识和客户导向思维'
  507. }
  508. Object.keys(candidateInfo.value.dimensionDetails).forEach(key => {
  509. const dimension = key as keyof typeof candidateInfo.value.dimensionDetails
  510. const rating = candidateInfo.value.dimensions[dimension]
  511. let detail = dimensionDetailsMapping[dimension] || ''
  512. if (rating === '优秀') {
  513. detail += '表现优秀,符合岗位要求。'
  514. } else if (rating === '中等') {
  515. detail += '表现一般,基本符合岗位要求。'
  516. } else {
  517. detail += '表现欠佳,需要进一步提升。'
  518. }
  519. candidateInfo.value.dimensionDetails[dimension] = detail
  520. })
  521. // 更新面试记录
  522. if (interview_progress && interview_progress.length > 0) {
  523. candidateInfo.value.interviewRecord = interview_progress
  524. .filter((q: any) => q.video_answer)
  525. .map((q: any) => {
  526. // 处理答案文本
  527. let answerText = q.video_answer?.transcript || '';
  528. let is_correct = false;
  529. // 如果有answer_data且包含选项,则使用选项文本
  530. if (q.answer_data && q.answer_data.selected_option_details && q.answer_data.selected_option_details.length > 0) {
  531. answerText = q.answer_data.selected_option_details
  532. .map((option: any) => option.option_text)
  533. .join(', ');
  534. is_correct = q.answer_data.selected_option_details
  535. .map((option: any) => option.is_correct)
  536. .join(', ');
  537. }
  538. console.log(q)
  539. return {
  540. question: q.question_text || '未提供问题',
  541. answer: answerText,
  542. analysis: q.video_answer?.ai_analysis?.comment || '',
  543. score: q.video_answer?.ai_score ? `${q.video_answer.ai_score}分` : '',
  544. question_form: q.question_form,
  545. is_correct:is_correct,
  546. videoUrl: q.video_answer?.video_url || '',
  547. question_image_url: q.question_image_url || '',
  548. options: q.options || [],
  549. thumbnail: ''
  550. }
  551. })
  552. }
  553. // 更新DUV分析
  554. if (application?.visual_analysis_results && application.visual_analysis_results.photo_results) {
  555. // 从photo_results中收集所有detections
  556. const allDetections: any[] = []
  557. application.visual_analysis_results.photo_results.forEach((photo: any) => {
  558. if (photo.detections && photo.detections.length > 0) {
  559. photo.detections.forEach((detection: any) => {
  560. allDetections.push({
  561. title: detection.feature || '特征分析',
  562. content: detection.location ?
  563. `在${detection.location}发现${detection.feature}${detection.description ? ',' + detection.description : ''}` :
  564. detection.feature + (detection.description ? ',' + detection.description : ''),
  565. score: detection.confidence >= 0.8 ? '确认' : '疑似',
  566. type: detection.description && detection.description.includes('影响') ? 'negative' : 'neutral'
  567. })
  568. })
  569. }
  570. })
  571. // 如果有检测结果,更新DUV分析
  572. if (allDetections.length > 0) {
  573. candidateInfo.value.duvAnalysis = allDetections
  574. } else {
  575. // 如果没有检测结果,提供默认值
  576. candidateInfo.value.duvAnalysis = [
  577. {
  578. title: '未发现特殊特征',
  579. content: '未在照片中检测到特殊特征',
  580. score: '正常',
  581. type: 'positive'
  582. }
  583. ]
  584. }
  585. } else if (application?.visual_analysis_results && application.visual_analysis_results.detections) {
  586. // 兼容旧版API格式
  587. candidateInfo.value.duvAnalysis = application.visual_analysis_results.detections.map((detection: any) => ({
  588. title: detection.feature || '特征分析',
  589. content: detection.location ? `在${detection.location}发现${detection.feature}` : detection.feature,
  590. score: detection.confidence >= 0.8 ? '确认' : '疑似',
  591. type: 'neutral'
  592. }))
  593. } else {
  594. // 如果没有视觉分析结果,提供默认值
  595. candidateInfo.value.duvAnalysis = [
  596. {
  597. title: '未进行DUV分析',
  598. content: '未提供DUV分析数据',
  599. score: '未知',
  600. type: 'neutral'
  601. }
  602. ]
  603. }
  604. // 更新手势验证和人脸验证图片
  605. if (posture_photos && posture_photos.length > 0) {
  606. const leftHandPhotos = posture_photos.filter((p: any) =>
  607. p.description.includes('left_') || p.description.includes('左手')
  608. ).map((p: any) => p.photo_url)
  609. if (leftHandPhotos.length > 0) {
  610. candidateInfo.value.additionalInfo.leftHandGestures.images = leftHandPhotos
  611. }
  612. const rightHandPhotos = posture_photos.filter((p: any) =>
  613. p.description.includes('right_') || p.description.includes('右手')
  614. ).map((p: any) => p.photo_url)
  615. if (rightHandPhotos.length > 0) {
  616. candidateInfo.value.additionalInfo.rightHandGestures.images = rightHandPhotos
  617. }
  618. const facePhotos = posture_photos.filter((p: any) =>
  619. p.description.includes('面部') ||
  620. (!p.description.includes('left_') &&
  621. !p.description.includes('right_') &&
  622. !p.description.includes('左手') &&
  623. !p.description.includes('右手'))
  624. ).map((p: any) => p.photo_url)
  625. if (facePhotos.length > 0) {
  626. candidateInfo.value.additionalInfo.faceVerification.images = facePhotos
  627. }
  628. }
  629. // 更新视频记录
  630. if (interview_progress && interview_progress.length > 0) {
  631. const videosByCategory: Record<string, any[]> = {}
  632. interview_progress.forEach((q: any) => {
  633. if (q.video_answer && q.video_answer.video_url) {
  634. const category = q.question_type_display || '面试视频'
  635. if (!videosByCategory[category]) {
  636. videosByCategory[category] = []
  637. }
  638. videosByCategory[category].push({
  639. url: q.video_answer.video_url,
  640. thumbnail: '/images/video-placeholder.jpg',
  641. description: q.question_text
  642. })
  643. }
  644. })
  645. candidateInfo.value.videoRecords = Object.entries(videosByCategory).map(([category, videos]) => ({
  646. category,
  647. videos
  648. }))
  649. }
  650. // 更新入职相关信息
  651. candidateInfo.value.availabilityPeriod = '无特殊情况'
  652. candidateInfo.value.onboardingTime = '随时可以入职'
  653. }
  654. onMounted(() => {
  655. console.log('完整URL:', window.location.href)
  656. console.log('路径:', window.location.pathname)
  657. console.log('查询字符串:', window.location.search)
  658. console.log('哈希:', window.location.hash)
  659. fetchApplicationDetail()
  660. // 确保所有主要部分都有正确的ID
  661. setTimeout(() => {
  662. console.log('检查页面ID:');
  663. ['comprehensive-assessment', 'image-analysis', 'answer-records', 'other-info'].forEach(id => {
  664. const element = document.getElementById(id);
  665. console.log(`ID ${id} 存在:`, !!element);
  666. });
  667. }, 1000);
  668. // 添加滚动监听
  669. window.addEventListener('scroll', () => {
  670. const sections = ['comprehensive-assessment', 'image-analysis', 'answer-records', 'other-info'];
  671. // 找到当前在视口中的部分
  672. for (const sectionId of sections) {
  673. const element = document.getElementById(sectionId);
  674. if (element) {
  675. const rect = element.getBoundingClientRect();
  676. if (rect.top <= 150 && rect.bottom >= 150) {
  677. setActiveTab(sectionId);
  678. break;
  679. }
  680. }
  681. }
  682. }, { passive: true });
  683. })
  684. const evaluationScore = ref<number>(0)
  685. const evaluationComments = ref<string>('')
  686. const formRef = ref<FormInstance>()
  687. const handleSubmit = async () => {
  688. try {
  689. await formRef.value?.validate()
  690. message.success('评估已提交')
  691. } catch (error) {
  692. message.error('请完成所有必填项')
  693. }
  694. }
  695. const reportTop = ref<HTMLElement | null>(null)
  696. const scrollToTop = () => {
  697. console.log('尝试滚动到顶部')
  698. // 使用ref引用直接滚动到顶部元素
  699. if (reportTop.value) {
  700. reportTop.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
  701. console.log('使用ref滚动到顶部')
  702. return
  703. }
  704. // 如果ref不可用,尝试通过ID查找元素
  705. const topElement = document.getElementById('report-top')
  706. if (topElement) {
  707. topElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
  708. console.log('使用ID滚动到顶部')
  709. return
  710. }
  711. // 如果以上方法都失败,使用之前的备用方法
  712. // ... 之前的代码 ...
  713. }
  714. const handleShare = () => {
  715. ElMessage.success('分享链接已复制')
  716. }
  717. const handleDownload = async () => {
  718. try {
  719. // 从路由获取参数
  720. const id = route.query.id as string || '1'
  721. const tenant_id = route.query.tenant_id as string || '1'
  722. const application_id = route.query.application_id as string || '1'
  723. // 显示下载开始的提示
  724. ElMessage.success('报告下载中...')
  725. // 调用下载接口
  726. const response = await fetch(`${import.meta.env.VITE_API_URL}/api/system/job/export_application_pdf?application_id=${id || application_id}&tenant_id=${tenant_id}`)
  727. const result = await response.json()
  728. if (result.code === 2000) {
  729. // 下载任务创建成功,提示用户去下载中心查看
  730. ElMessage.success('下载任务已创建,请前往下载中心查看')
  731. } else {
  732. // 处理错误情况
  733. ElMessage.error(result.msg || '下载任务创建失败')
  734. }
  735. } catch (error) {
  736. console.error('创建下载任务失败:', error)
  737. ElMessage.error('创建下载任务失败,请稍后重试')
  738. }
  739. }
  740. // 添加类型定义
  741. const originalButtonDisplay: string[] = []
  742. // 修改打印函数
  743. const handlePrint = () => {
  744. // 保存当前页面滚动位置
  745. const scrollPosition = window.scrollY
  746. // 临时修改样式以确保所有内容都能打印
  747. const originalOverflow = document.body.style.overflow
  748. const originalHeight = document.body.style.height
  749. document.body.style.overflow = 'visible'
  750. document.body.style.height = 'auto'
  751. // 隐藏不需要打印的元素
  752. const buttonsToHide = document.querySelectorAll('.fixed')
  753. buttonsToHide.forEach((button, index) => {
  754. // 添加类型断言
  755. const buttonElement = button as HTMLElement
  756. originalButtonDisplay[index] = buttonElement.style.display
  757. buttonElement.style.display = 'none'
  758. })
  759. // 延迟执行打印,确保样式已应用
  760. setTimeout(() => {
  761. window.print()
  762. // 打印完成后恢复原始样式
  763. document.body.style.overflow = originalOverflow
  764. document.body.style.height = originalHeight
  765. // 恢复隐藏的按钮
  766. buttonsToHide.forEach((button, index) => {
  767. // 添加类型断言
  768. const buttonElement = button as HTMLElement
  769. buttonElement.style.display = originalButtonDisplay[index]
  770. })
  771. // 恢复滚动位置
  772. window.scrollTo(0, scrollPosition)
  773. ElMessage.success('打印完成')
  774. }, 300)
  775. }
  776. // 添加base64编码的内联图片作为fallback
  777. const fallbackImageBase64Ref = ref('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2VlZWVlZSIvPjxwYXRoIGQ9Ik03NSA2NUExMCAxMCAwIDAgMSA4NSA3NUExMCAxMCAwIDAgMSA3NSA4NUExMCAxMCAwIDAgMSA2NSA3NUExMCAxMCAwIDAgMSA3NSA2NVoiIGZpbGw9IiNhYWFhYWEiLz48cGF0aCBkPSJNMTIwIDEzNUw5MCAxMDVMNjAgMTM1TDYwIDE0MEwxNDAgMTQwTDE0MCAxMzVMMTIwIDExNVoiIGZpbGw9IiNhYWFhYWEiLz48L3N2Zz4=')
  778. // 简化的图片错误处理函数
  779. const handleImageError = (event: Event) => {
  780. const target = event.target as HTMLImageElement
  781. if (target) {
  782. target.src = fallbackImageBase64Ref.value
  783. target.onerror = null // 防止无限循环
  784. }
  785. }
  786. // 修改视频加载和第一帧捕获的函数
  787. const captureVideoFirstFrame = (videoElement: HTMLVideoElement) => {
  788. // 创建一个一次性的事件监听器,在视频可以播放时捕获第一帧
  789. const captureFrame = () => {
  790. try {
  791. // 确保视频已加载足够的数据
  792. if (videoElement.readyState >= 2) {
  793. // 创建canvas元素
  794. const canvas = document.createElement('canvas')
  795. canvas.width = videoElement.videoWidth || 320
  796. canvas.height = videoElement.videoHeight || 240
  797. // 在canvas上绘制视频当前帧
  798. const ctx = canvas.getContext('2d')
  799. if (ctx) {
  800. // 确保视频当前时间设置为开始位置
  801. videoElement.currentTime = 0.1
  802. // 绘制视频帧到canvas
  803. ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height)
  804. // 将canvas转换为图片URL
  805. const thumbnailUrl = canvas.toDataURL('image/jpeg')
  806. // 设置为视频的poster属性
  807. videoElement.poster = thumbnailUrl
  808. // 清理
  809. videoElement.removeEventListener('loadeddata', captureFrame)
  810. }
  811. }
  812. } catch (error) {
  813. console.error('捕获视频第一帧失败:', error)
  814. // 设置默认缩略图
  815. videoElement.poster = fallbackImageBase64Ref.value
  816. }
  817. }
  818. // 添加事件监听器
  819. videoElement.addEventListener('loadeddata', captureFrame)
  820. // 如果视频已经加载,立即尝试捕获
  821. if (videoElement.readyState >= 2) {
  822. captureFrame()
  823. }
  824. // 添加错误处理
  825. videoElement.addEventListener('error', () => {
  826. console.error('视频加载失败')
  827. videoElement.poster = fallbackImageBase64Ref.value
  828. })
  829. // 设置超时,确保即使视频加载缓慢也能显示默认缩略图
  830. setTimeout(() => {
  831. if (!videoElement.poster || videoElement.poster === '') {
  832. videoElement.poster = fallbackImageBase64Ref.value
  833. }
  834. }, 3000)
  835. }
  836. // 视频加载完成后的处理函数
  837. const handleVideoLoaded = (event: Event) => {
  838. const videoElement = event.target as HTMLVideoElement
  839. if (videoElement) {
  840. // 确保视频已经加载元数据
  841. if (videoElement.readyState >= 1) {
  842. captureVideoFirstFrame(videoElement)
  843. } else {
  844. // 如果元数据尚未加载,添加元数据加载事件
  845. videoElement.addEventListener('loadedmetadata', () => {
  846. captureVideoFirstFrame(videoElement)
  847. })
  848. }
  849. }
  850. }
  851. // 添加视频错误处理函数
  852. const handleVideoError = (event: Event) => {
  853. const videoElement = event.target as HTMLVideoElement
  854. if (videoElement) {
  855. videoElement.poster = fallbackImageBase64Ref.value
  856. console.error('视频加载错误')
  857. }
  858. }
  859. // 修改 DUV 得分解释函数
  860. const getDuvScoreInterpretation = (score: number | undefined): string => {
  861. if (score === undefined) return '未进行心理测评或数据缺失'
  862. if (score <= 20) {
  863. return '心理状态健康,情绪稳定、认知正常。候选人在心理测评中表现出稳定的情绪状态和健康的心理特质,适合岗位要求。'
  864. } else if (score <= 35) {
  865. return '轻微焦虑或负面情绪,仍具备岗位适应力。候选人在某些方面可能存在轻微的心理压力,但总体上仍保持稳定,基本符合岗位要求。'
  866. } else if (score <= 55) {
  867. return '存在一定情绪波动或压力反应。候选人在测评中表现出一定程度的心理压力或情绪波动,建议进一步面谈评估。'
  868. } else if (score <= 70) {
  869. return '心理压力较大,可能影响工作稳定性。候选人在测评中表现出较明显的心理压力或情绪不稳定性,建议慎重录用或考虑调岗。'
  870. } else {
  871. return '可能存在较严重心理或精神问题。候选人在测评中表现出明显的心理或情绪问题,不建议录用。'
  872. }
  873. }
  874. // 更新 DUV 得分显示的 CSS 类判断逻辑
  875. const getDuvScoreClass = (score: number | undefined): string => {
  876. if (score === undefined) return 'text-gray-500'
  877. if (score <= 20) {
  878. return 'text-green-500' // 优秀
  879. } else if (score <= 35) {
  880. return 'text-blue-500' // 良好
  881. } else if (score <= 55) {
  882. return 'text-yellow-500' // 一般
  883. } else if (score <= 70) {
  884. return 'text-orange-500' // 偏差
  885. } else {
  886. return 'text-red-500' // 严重异常
  887. }
  888. }
  889. // 获取 DUV 得分等级
  890. const getDuvScoreLevel = (score: number | undefined): string => {
  891. if (score === undefined) return '未知'
  892. if (score <= 20) {
  893. return '优秀'
  894. } else if (score <= 35) {
  895. return '良好'
  896. } else if (score <= 55) {
  897. return '一般'
  898. } else if (score <= 70) {
  899. return '偏差'
  900. } else {
  901. return '严重异常'
  902. }
  903. }
  904. // 获取录用建议
  905. const getDuvHireRecommendation = (score: number | undefined): string => {
  906. if (score === undefined) return '无法给出建议'
  907. if (score <= 20) {
  908. return '建议优先录取'
  909. } else if (score <= 35) {
  910. return '可考虑录取'
  911. } else if (score <= 55) {
  912. return '建议进一步面谈评估'
  913. } else if (score <= 70) {
  914. return '慎重录用或建议调岗'
  915. } else {
  916. return '不建议录用'
  917. }
  918. }
  919. // 添加安全获取分数值的辅助函数
  920. const getSafeScoreValue = (score: number | undefined): number => {
  921. return score !== undefined ? score : 0
  922. }
  923. const getSafeMaxScoreValue = (maxScore: number | undefined): number => {
  924. return maxScore !== undefined ? maxScore : 210
  925. }
  926. // 用户个人信息状态
  927. const userProfileData = ref<UserProfile | null>(null);
  928. const userProfileDialogVisible = ref(false);
  929. const userProfileLoading = ref(false);
  930. // 获取用户个人信息
  931. const fetchUserProfile = async () => {
  932. userProfileLoading.value = true;
  933. try {
  934. // 从应用数据中获取用户ID和租户ID
  935. const userId = apiData.value?.applicant?.id || route.query.user_id || '30';
  936. const tenantId = route.query.tenant_id || '1';
  937. const response = await fetch(`${import.meta.env.VITE_API_URL}/api/system/wechat/user/profile/get?user_id=${userId}&tenant_id=${tenantId}`);
  938. const result = await response.json();
  939. if (result.code === 2000) {
  940. userProfileData.value = result.data;
  941. userProfileDialogVisible.value = true;
  942. } else {
  943. ElMessage.error(result.msg || '获取用户信息失败');
  944. }
  945. } catch (error) {
  946. console.error('获取用户个人信息失败:', error);
  947. ElMessage.error('获取用户信息失败,请稍后重试');
  948. } finally {
  949. userProfileLoading.value = false;
  950. }
  951. };
  952. // 格式化日期显示
  953. const formatDate = (dateString: string | null | undefined): string => {
  954. if (!dateString) return '未设置';
  955. try {
  956. const date = new Date(dateString);
  957. return date.toLocaleDateString('zh-CN');
  958. } catch (e) {
  959. return dateString;
  960. }
  961. };
  962. // 处理查看个人信息按钮点击
  963. const handleViewProfile = () => {
  964. fetchUserProfile();
  965. };
  966. const handleTabClick = (tab: any) => {
  967. console.log('点击标签:', tab.props.name);
  968. // 尝试多种方法找到目标元素
  969. const targetId = tab.props.name;
  970. const element = document.getElementById(targetId);
  971. if (element) {
  972. // 找到滚动容器 - 可能是document.body, document.documentElement, 或特定容器
  973. const scrollContainer = document.querySelector('.max-w-4xl') ||
  974. document.documentElement ||
  975. document.body;
  976. // 计算元素相对于滚动容器的位置
  977. const containerRect = scrollContainer.getBoundingClientRect();
  978. const elementRect = element.getBoundingClientRect();
  979. const relativeTop = elementRect.top - containerRect.top;
  980. // 滚动容器
  981. if (scrollContainer === document.documentElement || scrollContainer === document.body) {
  982. window.scrollTo({
  983. top: window.scrollY + relativeTop - 100, // 减去100px的偏移
  984. behavior: 'smooth'
  985. });
  986. } else {
  987. scrollContainer.scrollTo({
  988. top: scrollContainer.scrollTop + relativeTop - 100,
  989. behavior: 'smooth'
  990. });
  991. }
  992. console.log('执行滚动:', window.scrollY + relativeTop - 100);
  993. } else {
  994. console.error('未找到目标元素:', targetId);
  995. }
  996. };
  997. // 添加一个备用的滚动函数,可以通过按钮直接调用
  998. /* const scrollToSection = (sectionId: string) => {
  999. console.log('尝试滚动到部分:', sectionId);
  1000. const element = document.getElementById(sectionId);
  1001. if (element) {
  1002. element.scrollIntoView({ behavior: 'smooth', block: 'start' });
  1003. setTimeout(() => {
  1004. window.scrollBy({ top: -100, behavior: 'smooth' });
  1005. }, 100);
  1006. }
  1007. }; */
  1008. // 添加到methods部分或直接在setup中定义
  1009. const setActiveTab = (sectionId) => {
  1010. // 移除所有标签的active类
  1011. document.querySelectorAll('.tab-item').forEach(tab => {
  1012. tab.classList.remove('active');
  1013. });
  1014. // 根据sectionId找到对应的标签并添加active类
  1015. const tabMap = {
  1016. 'comprehensive-assessment': 0,
  1017. 'image-analysis': 1,
  1018. 'answer-records': 2,
  1019. 'other-info': 3
  1020. };
  1021. const index = tabMap[sectionId];
  1022. if (index !== undefined) {
  1023. document.querySelectorAll('.tab-item')[index].classList.add('active');
  1024. }
  1025. };
  1026. // 修改scrollToSection函数
  1027. const scrollToSection = (sectionId) => {
  1028. console.log('尝试滚动到部分:', sectionId);
  1029. const element = document.getElementById(sectionId);
  1030. if (element) {
  1031. element.scrollIntoView({ behavior: 'smooth', block: 'start' });
  1032. setTimeout(() => {
  1033. window.scrollBy({ top: -100, behavior: 'smooth' });
  1034. setActiveTab(sectionId); // 设置激活的标签
  1035. }, 100);
  1036. }
  1037. };
  1038. </script>
  1039. <template>
  1040. <!-- 修改最外层容器的类,添加背景样式 -->
  1041. <div class="min-h-screen bg-gradient-custom">
  1042. <div class="max-w-4xl mx-auto p-6 relative overflow-y-auto" style="height: 100vh;">
  1043. <!-- 添加顶部锚点 -->
  1044. <div id="report-top" ref="reportTop"></div>
  1045. <!-- 加载状态 -->
  1046. <a-spin :spinning="loading" tip="加载中...">
  1047. <!-- 页面标题 -->
  1048. <div class="bg-[#1d1e3a] text-white p-4 rounded-t-lg">
  1049. <div class="flex justify-between items-center">
  1050. <div class="text-sm">No.{{ apiData?.application?.report_id || candidateInfo.idNumber }}</div>
  1051. <div class="text-sm">{{ new Date().toLocaleString() }}</div>
  1052. </div>
  1053. </div>
  1054. <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
  1055. <strong class="font-bold">错误!</strong>
  1056. <span class="block sm:inline">{{ error }}</span>
  1057. </div>
  1058. <div v-else class="bg-white rounded-b-lg shadow-lg p-6">
  1059. <!-- 头部信息 -->
  1060. <div class="flex justify-between items-center mb-8 pb-4 border-b">
  1061. <div class="flex items-center space-x-4">
  1062. <div class="w-20 h-20 bg-gray-200 rounded-lg overflow-hidden">
  1063. <img
  1064. :src="candidateInfo.additionalInfo.faceVerification.images[0]"
  1065. alt="头像"
  1066. class="w-full h-full object-cover"
  1067. @error="handleImageError"
  1068. >
  1069. </div>
  1070. <div>
  1071. <div class="flex items-center gap-2">
  1072. <h1 class="text-2xl font-bold">{{ candidateInfo.name }}</h1>
  1073. <el-button size="small" style="color: #1d1e3a; text-decoration: underline;" type="text" @click="handleViewProfile">
  1074. 个人信息
  1075. </el-button>
  1076. </div>
  1077. <p class="text-gray-600">{{ candidateInfo.phoneNumber }}</p>
  1078. <p v-if="apiData?.position" class="text-gray-500 text-sm">
  1079. 应聘职位: {{ apiData.position.title }} - {{ apiData.position.department }}
  1080. </p>
  1081. </div>
  1082. </div>
  1083. <div class="text-right">
  1084. <div class="text-4xl font-bold text-yellow-500">{{ candidateInfo.score }}<span class="text-base ml-1">分</span></div>
  1085. <div class="text-sm text-yellow-500">
  1086. {{ candidateInfo.score >= 70 ? '推荐 👍' : candidateInfo.score >= 60 ? '待定 🤔' : '不推荐 👎' }}
  1087. </div>
  1088. </div>
  1089. </div>
  1090. <!-- 基本信息 -->
  1091. <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
  1092. <div>
  1093. <label class="text-gray-600">身份证号</label>
  1094. <div>{{ candidateInfo.idNumber }}</div>
  1095. </div>
  1096. <!-- <div>
  1097. <label class="text-gray-600">入职之后,您缺岗的班次时间段是?</label>
  1098. <div>{{ candidateInfo.availabilityPeriod }}</div>
  1099. </div>
  1100. <div>
  1101. <label class="text-gray-600">请问您通过面试之后多久可以入职?</label>
  1102. <div>{{ candidateInfo.onboardingTime }}</div>
  1103. </div> -->
  1104. <div>
  1105. <label class="text-gray-600">薪资范围</label>
  1106. <div>{{ candidateInfo.salaryRange }}</div>
  1107. </div>
  1108. <div>
  1109. <label class="text-gray-600">建议薪资</label>
  1110. <div class="font-semibold text-blue-600">{{ candidateInfo.suggestedSalary }}</div>
  1111. </div>
  1112. </div>
  1113. <!-- 在标签导航下方添加备用导航按钮 -->
  1114. <!-- <div class="backup-navigation mt-4 mb-6 flex flex-wrap gap-2">
  1115. <el-button size="small" @click="scrollToSection('comprehensive-assessment')">
  1116. 综合评估
  1117. </el-button>
  1118. <el-button size="small" @click="scrollToSection('image-analysis')">
  1119. 图片分析
  1120. </el-button>
  1121. <el-button size="small" @click="scrollToSection('answer-records')">
  1122. 回答记录
  1123. </el-button>
  1124. <el-button size="small" @click="scrollToSection('other-info')">
  1125. 其他信息
  1126. </el-button>
  1127. </div> -->
  1128. <!-- 将原来的标签导航移到更上层,并添加固定定位 -->
  1129. <div class="sticky-tabs">
  1130. <div class="custom-tabs">
  1131. <div class="tab-item active" @click="scrollToSection('comprehensive-assessment')">
  1132. <span>综合评估</span>
  1133. </div>
  1134. <div class="tab-item" @click="scrollToSection('image-analysis')">
  1135. <span>图片分析评估</span>
  1136. </div>
  1137. <div class="tab-item" @click="scrollToSection('answer-records')">
  1138. <span>回答记录</span>
  1139. </div>
  1140. <div class="tab-item" @click="scrollToSection('other-info')">
  1141. <span>其他信息</span>
  1142. </div>
  1143. </div>
  1144. </div>
  1145. <!-- 为每个主要章节添加id -->
  1146. <div id="comprehensive-assessment" class="mb-8 pt-4 border-t border-transparent">
  1147. <!-- 1. 综合评估 -->
  1148. <div class="mb-8">
  1149. <h2 class="text-xl font-bold mb-6">1. 综合评估</h2>
  1150. <div class="space-y-4">
  1151. <!-- 为每个部分添加 id -->
  1152. <div id="hire-recommendation" class="border-b pb-4">
  1153. <!-- 录用建议部分的现有代码 -->
  1154. <div class="border-b pb-4">
  1155. <div class="flex items-center justify-between mb-2">
  1156. <span class="text-gray-600">录用建议</span>
  1157. <span :class="{
  1158. 'text-green-500': candidateInfo.hireRecommendation?.includes('推荐'),
  1159. 'text-red-500': candidateInfo.hireRecommendation?.includes('不推荐'),
  1160. 'text-yellow-500': !candidateInfo.hireRecommendation?.includes('推荐') && !candidateInfo.hireRecommendation?.includes('不推荐')
  1161. }">{{ candidateInfo.hireRecommendation || '无建议' }}</span>
  1162. </div>
  1163. <p class="text-gray-600 text-sm">{{ candidateInfo.hireReason || '无详细说明' }}</p>
  1164. </div>
  1165. </div>
  1166. <div id="red-line" class="border-b pb-4">
  1167. <!-- 红线题检测部分的现有代码 -->
  1168. <div class="flex items-center justify-between mb-2">
  1169. <span class="text-gray-600" style="color: red;">红线题检测结果</span>
  1170. <span :class="{
  1171. 'text-green-500': apiData?.required_questions_summary?.all_correct,
  1172. 'text-red-500': apiData?.required_questions_summary && !apiData.required_questions_summary.all_correct
  1173. }">
  1174. {{ apiData?.required_questions_summary?.all_correct ? '全部正确' : '存在错误,建议不录用' }}
  1175. </span>
  1176. </div>
  1177. <div v-if="apiData?.required_questions_summary && !apiData.required_questions_summary.all_correct"
  1178. class="mt-2 border-l-4 border-red-500 pl-3 bg-red-50 p-2 rounded">
  1179. <p class="text-red-700 font-medium mb-1">未通过的红线题:</p>
  1180. <ul v-if="apiData.required_questions_summary.failed_questions_details.length > 0"
  1181. class="list-disc pl-5 text-gray-600 text-sm">
  1182. <li v-for="(question, index) in apiData.required_questions_summary.failed_questions_details"
  1183. :key="'failed-'+index">
  1184. {{ question.question_text || `问题 ${index + 1}` }}
  1185. </li>
  1186. </ul>
  1187. <p v-else class="text-gray-600 text-sm">未提供详细信息</p>
  1188. </div>
  1189. </div>
  1190. <div id="strengths" v-if="candidateInfo.strengths && candidateInfo.strengths.length > 0" class="border-b pb-4">
  1191. <h3 class="font-semibold mb-2">优点</h3>
  1192. <ul class="list-disc pl-5 text-gray-600 text-sm">
  1193. <li v-for="(strength, index) in candidateInfo.strengths" :key="'strength-'+index">
  1194. {{ strength }}
  1195. </li>
  1196. </ul>
  1197. </div>
  1198. <div id="weaknesses" v-if="candidateInfo.weaknesses && candidateInfo.weaknesses.length > 0" class="border-b pb-4">
  1199. <h3 class="font-semibold mb-2">需改进的地方</h3>
  1200. <ul class="list-disc pl-5 text-gray-600 text-sm">
  1201. <li v-for="(weakness, index) in candidateInfo.weaknesses" :key="'weakness-'+index">
  1202. {{ weakness }}
  1203. </li>
  1204. </ul>
  1205. </div>
  1206. <div id="duv-analysis" class="border-b pb-4">
  1207. <!-- DUV心理测评部分的现有代码 -->
  1208. <div class="flex items-center justify-between mt-4 mb-2">
  1209. <span class="text-gray-600">DUV 心理测评得分</span>
  1210. <span :class="getDuvScoreClass(apiData?.scoring_summary?.total_score_obtained)">
  1211. {{ getSafeScoreValue(apiData?.scoring_summary?.total_score_obtained) }}
  1212. <span class="ml-2 text-sm">({{ getDuvScoreLevel(apiData?.scoring_summary?.total_score_obtained) }})</span>
  1213. </span>
  1214. </div>
  1215. <p class="text-gray-600 text-sm">
  1216. {{ getDuvScoreInterpretation(apiData?.scoring_summary?.total_score_obtained) }}
  1217. </p>
  1218. <p class="text-sm mt-1" :class="getDuvScoreClass(apiData?.scoring_summary?.total_score_obtained)">
  1219. <strong>录用建议:</strong> {{ getDuvHireRecommendation(apiData?.scoring_summary?.total_score_obtained) }}
  1220. </p>
  1221. </div>
  1222. </div>
  1223. </div>
  1224. </div>
  1225. <div id="image-analysis" class="mb-8 pt-4 border-t border-transparent">
  1226. <h2 class="text-xl font-bold mb-6">2. 图片分析评估</h2>
  1227. <div class="space-y-4">
  1228. <div v-for="(item, index) in candidateInfo.duvAnalysis" :key="index" class="border-b pb-4">
  1229. <div class="flex items-center justify-between mb-2">
  1230. <span class="text-gray-600">{{ item.title }}</span>
  1231. <span :class="{
  1232. 'text-green-500': item.type === 'positive',
  1233. 'text-red-500': item.type === 'negative',
  1234. 'text-yellow-500': item.type === 'neutral'
  1235. }">{{ item.score }}</span>
  1236. </div>
  1237. <p class="text-gray-600 text-sm">{{ item.content }}</p>
  1238. </div>
  1239. </div>
  1240. </div>
  1241. <div id="answer-records" class="mb-8 pt-4 border-t border-transparent">
  1242. <h2 class="text-xl font-bold mb-6">3. 回答记录</h2>
  1243. <div class="space-y-4">
  1244. <div v-for="(record, index) in candidateInfo.interviewRecord" :key="index" class="bg-gray-50 rounded-lg p-4">
  1245. <!-- 题目标题部分 -->
  1246. <div class="mb-4">
  1247. <div class="flex items-start space-x-2">
  1248. <div class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">第{{ index + 1 }}题</div>
  1249. <div class="flex-1">
  1250. <h3 class="font-semibold text-gray-800">{{ record.question }}</h3>
  1251. </div>
  1252. </div>
  1253. <!-- 视频问答题布局 -->
  1254. <div v-if="record.question_form === 0" class="mt-4 flex space-x-4">
  1255. <!-- 右侧答案和评分 -->
  1256. <div class="w-2/3">
  1257. <div class="bg-white p-4 rounded-lg">
  1258. <div class="mb-4">
  1259. <div class="flex items-start space-x-2">
  1260. <div class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">候选人</div>
  1261. <div class="flex-1">
  1262. <p class="text-gray-700">{{ record.answer }}</p>
  1263. </div>
  1264. </div>
  1265. </div>
  1266. <div>
  1267. <div class="flex items-start space-x-2">
  1268. <div class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm">AI评分</div>
  1269. <div class="flex-1">
  1270. <p class="text-gray-600 m-0">{{ record.analysis }}</p>
  1271. <div class="mt-1 text-blue-500">得分:{{ record.score }}</div>
  1272. </div>
  1273. </div>
  1274. </div>
  1275. </div>
  1276. </div>
  1277. <!-- 左侧视频播放器 -->
  1278. <div class="w-1/3">
  1279. <div class="video-container">
  1280. <video
  1281. class="w-full h-full object-cover rounded-lg"
  1282. controls
  1283. :src="record.videoUrl"
  1284. preload="metadata"
  1285. :poster="fallbackImageBase64Ref"
  1286. @loadeddata="handleVideoLoaded"
  1287. @error="handleVideoError"
  1288. >
  1289. 您的浏览器不支持视频播放。
  1290. </video>
  1291. </div>
  1292. </div>
  1293. </div>
  1294. <!-- 选择题布局 -->
  1295. <div v-else-if="record.question_form === 1 || record.question_form === 2 || record.question_form === 4" class="mt-4">
  1296. <div class="bg-white p-4 rounded-lg">
  1297. <div class="flex items-center justify-between mb-3">
  1298. <div class="flex items-center space-x-2">
  1299. <div class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">候选人:</div>
  1300. <p class="text-gray-700" style="margin: 0;">{{ record.answer }}</p>
  1301. </div>
  1302. <div>
  1303. <span v-if="record.question_form === 1 || record.question_form === 2" :class="[
  1304. 'px-3 py-1 rounded-full text-sm',
  1305. record.is_correct === true || record.is_correct === 'true'
  1306. ? 'bg-green-100 text-green-800'
  1307. : 'bg-red-100 text-red-800'
  1308. ]">
  1309. {{ record.is_correct === true || record.is_correct === 'true' ? '正确' : '错误' }}
  1310. </span>
  1311. </div>
  1312. </div>
  1313. <!-- 添加选项渲染 -->
  1314. <div v-if="record.options && record.options.length > 0" class="mt-3 space-y-2">
  1315. <div class="text-sm text-gray-500 mb-1">所有选项:</div>
  1316. <div v-for="(option, optIndex) in record.options" :key="optIndex"
  1317. :class="[
  1318. 'p-2 rounded border',
  1319. record.answer.includes(option.option_text)
  1320. ? 'border-blue-300 bg-blue-50'
  1321. : 'border-gray-200'
  1322. ]">
  1323. <div class="flex items-start">
  1324. <div class="mr-2 text-gray-500">{{ String.fromCharCode(65 + optIndex) }}.</div>
  1325. <div class="flex-1">
  1326. <div class="text-gray-700">{{ option.option_text }}</div>
  1327. <div v-if="option.is_correct" class="text-xs text-green-600 mt-1">✓ 正确答案</div>
  1328. </div>
  1329. </div>
  1330. </div>
  1331. </div>
  1332. </div>
  1333. </div>
  1334. <!-- 图片题布局 -->
  1335. <div v-else-if="record.question_form === 3" class="mt-4 flex space-x-4">
  1336. <!-- 左侧答案部分 -->
  1337. <div class="w-1/3">
  1338. <div class="bg-white p-4 rounded-lg">
  1339. <div class="flex items-start space-x-2">
  1340. <div class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">候选人</div>
  1341. <div class="flex-1">
  1342. <p class="text-gray-700" style="margin: 0;">{{ record.answer }}</p>
  1343. <div class="mt-2">
  1344. <span :class="[
  1345. 'px-3 py-1 rounded-full text-sm',
  1346. record.is_correct === true || record.is_correct === 'true'
  1347. ? 'bg-green-100 text-green-800'
  1348. : 'bg-red-100 text-red-800'
  1349. ]">
  1350. {{ record.is_correct === true || record.is_correct === 'true' ? '正确' : '错误' }}
  1351. </span>
  1352. </div>
  1353. </div>
  1354. </div>
  1355. </div>
  1356. </div>
  1357. <!-- 右侧图片显示 -->
  1358. <div class="w-2/3">
  1359. <div class=" rounded-lg overflow-hidden bg-gray-100">
  1360. <img
  1361. :src="record.question_image_url"
  1362. alt="题目图片"
  1363. class="w-full h-full object-cover rounded-lg"
  1364. @error="handleImageError"
  1365. >
  1366. </div>
  1367. </div>
  1368. </div>
  1369. </div>
  1370. </div>
  1371. </div>
  1372. </div>
  1373. <div id="other-info" class="mb-8 pt-4 border-t border-transparent">
  1374. <h2 class="text-xl font-bold mb-6">4. 其他信息</h2>
  1375. <div class="space-y-6">
  1376. <!-- 验证状态列表 -->
  1377. <div class="space-y-2">
  1378. <div class="flex items-center space-x-2">
  1379. <span class="text-gray-600">身份认证:</span>
  1380. <span class="text-green-500">{{ candidateInfo.additionalInfo.idVerification }}</span>
  1381. </div>
  1382. <div class="flex items-center space-x-2">
  1383. <span class="text-gray-600">手机号实名认证:</span>
  1384. <span class="text-green-500">{{ candidateInfo.additionalInfo.phoneVerification }}</span>
  1385. </div>
  1386. <div class="flex items-center space-x-2">
  1387. <span class="text-gray-600">综合A类评估:</span>
  1388. <span class="text-green-500">{{ candidateInfo.additionalInfo.comprehensiveVerification }}</span>
  1389. </div>
  1390. <div class="flex items-center space-x-2">
  1391. <span class="text-gray-600">综合B类评估:</span>
  1392. <span class="text-green-500">{{ candidateInfo.additionalInfo.comprehensiveVerification }}</span>
  1393. </div>
  1394. </div>
  1395. <!-- 手部综合检测 -->
  1396. <div class="space-y-4">
  1397. <div class="flex items-center space-x-2">
  1398. <span class="text-gray-600">手部综合检测:</span>
  1399. <span class="text-green-500">{{ candidateInfo.additionalInfo.handVerification }}</span>
  1400. </div>
  1401. <!-- 手部检测详细信息 -->
  1402. <div class="pl-4 space-y-2">
  1403. <div class="flex items-center space-x-2">
  1404. <span class="text-gray-600">手指灵活度:</span>
  1405. <span class="text-green-500">{{ candidateInfo.additionalInfo.handBrightness }}</span>
  1406. </div>
  1407. <div class="flex items-center space-x-2">
  1408. <span class="text-gray-600">手指完整度:</span>
  1409. <span class="text-green-500">{{ candidateInfo.additionalInfo.handIntegrity }}</span>
  1410. </div>
  1411. <div class="flex items-center space-x-2">
  1412. <span class="text-gray-600">手部姿态纹身:</span>
  1413. <span class="text-green-500">{{ candidateInfo.additionalInfo.handPoseRecognition }}</span>
  1414. </div>
  1415. </div>
  1416. <!-- 手势验证图片展示 -->
  1417. <div class="grid grid-cols-2 gap-8 mt-4">
  1418. <!-- 左手验证结果 -->
  1419. <div>
  1420. <div class="mb-2">
  1421. <h4 class="text-sm font-medium">左手识别结果</h4>
  1422. <div class="space-y-1">
  1423. <div class="flex items-center space-x-2">
  1424. <span>手指灵活度</span>
  1425. <span class="text-green-500">✓</span>
  1426. </div>
  1427. <div class="flex items-center space-x-2">
  1428. <span>手指完整度</span>
  1429. <span class="text-green-500">✓</span>
  1430. </div>
  1431. <div class="flex items-center space-x-2">
  1432. <span>手部姿态纹身</span>
  1433. <span class="text-green-500">✓</span>
  1434. </div>
  1435. </div>
  1436. </div>
  1437. <div class="grid grid-cols-3 gap-2">
  1438. <div v-for="(image, index) in candidateInfo.additionalInfo.leftHandGestures.images"
  1439. :key="index"
  1440. class="aspect-square rounded-lg overflow-hidden bg-gray-100">
  1441. <img :src="image"
  1442. alt="左手手势"
  1443. class="w-full h-full object-cover"
  1444. @error="handleImageError">
  1445. </div>
  1446. </div>
  1447. </div>
  1448. <!-- 右手验证结果 -->
  1449. <div>
  1450. <div class="mb-2">
  1451. <h4 class="text-sm font-medium">右手识别结果</h4>
  1452. <div class="space-y-1">
  1453. <div class="flex items-center space-x-2">
  1454. <span>手指灵活度</span>
  1455. <span class="text-green-500">✓</span>
  1456. </div>
  1457. <div class="flex items-center space-x-2">
  1458. <span>手指完整度</span>
  1459. <span class="text-green-500">✓</span>
  1460. </div>
  1461. <div class="flex items-center space-x-2">
  1462. <span>手部姿态纹身</span>
  1463. <span class="text-green-500">✓</span>
  1464. </div>
  1465. </div>
  1466. </div>
  1467. <div class="grid grid-cols-3 gap-2">
  1468. <div v-for="(image, index) in candidateInfo.additionalInfo.rightHandGestures.images"
  1469. :key="index"
  1470. class="aspect-square rounded-lg overflow-hidden bg-gray-100">
  1471. <img :src="image"
  1472. alt="右手手势"
  1473. class="w-full h-full object-cover"
  1474. @error="handleImageError">
  1475. </div>
  1476. </div>
  1477. </div>
  1478. </div>
  1479. </div>
  1480. <!-- 换人检测 -->
  1481. <div>
  1482. <h3 class="font-semibold mb-4">人脸验证: </h3>
  1483. <div class="grid grid-cols-3 md:grid-cols-6 gap-2">
  1484. <div v-for="(image, index) in candidateInfo.additionalInfo.faceVerification.images"
  1485. :key="index"
  1486. class="aspect-square rounded-lg overflow-hidden bg-gray-100">
  1487. <img :src="image"
  1488. alt="人脸验证"
  1489. class="w-full h-full object-cover"
  1490. @error="handleImageError">
  1491. </div>
  1492. </div>
  1493. </div>
  1494. </div>
  1495. </div>
  1496. </div>
  1497. </a-spin>
  1498. <!-- 悬浮按钮 (Element Plus 版本) -->
  1499. <div class="fixed right-8 bottom-24 flex flex-col space-y-4">
  1500. <el-button
  1501. type="primary"
  1502. style="margin-left: 12px;"
  1503. circle
  1504. @click="handleViewProfile"
  1505. >
  1506. <el-icon><User /></el-icon>
  1507. </el-button>
  1508. <el-button
  1509. type="primary"
  1510. style="margin-left: 12px;"
  1511. circle
  1512. @click="handleShare"
  1513. >
  1514. <el-icon><Share /></el-icon>
  1515. </el-button>
  1516. <el-button
  1517. type="primary"
  1518. circle
  1519. @click="handleDownload"
  1520. >
  1521. <el-icon><Download /></el-icon>
  1522. </el-button>
  1523. <el-button
  1524. type="primary"
  1525. circle
  1526. @click="handlePrint"
  1527. >
  1528. <el-icon><Printer /></el-icon>
  1529. </el-button>
  1530. <el-button
  1531. type="primary"
  1532. circle
  1533. @click="scrollToTop"
  1534. >
  1535. <el-icon><ArrowUp /></el-icon>
  1536. </el-button>
  1537. </div>
  1538. </div>
  1539. </div>
  1540. <!-- 用户个人信息弹窗 -->
  1541. <el-dialog
  1542. v-model="userProfileDialogVisible"
  1543. title="个人详细信息"
  1544. width="80%"
  1545. :close-on-click-modal="false"
  1546. class="user-profile-dialog"
  1547. >
  1548. <el-scrollbar height="70vh">
  1549. <el-spin :spinning="userProfileLoading">
  1550. <div v-if="userProfileData" class="user-profile-content">
  1551. <!-- 基本信息 -->
  1552. <div class="profile-section">
  1553. <h3 class="section-title">基本信息</h3>
  1554. <el-descriptions :column="3" border>
  1555. <el-descriptions-item label="姓名">{{ userProfileData.user_info.name }}</el-descriptions-item>
  1556. <el-descriptions-item label="手机号">{{ userProfileData.user_info.phone }}</el-descriptions-item>
  1557. <el-descriptions-item label="年龄">{{ userProfileData.user_info.age }}岁</el-descriptions-item>
  1558. <el-descriptions-item label="出生日期">{{ formatDate(userProfileData.user_info.birth_date) }}</el-descriptions-item>
  1559. <el-descriptions-item label="性别">{{ userProfileData.user_info.gender_name }}</el-descriptions-item>
  1560. <el-descriptions-item label="身份证号">{{ userProfileData.user_info.id_card }}</el-descriptions-item>
  1561. </el-descriptions>
  1562. </div>
  1563. <!-- 个人资料 -->
  1564. <div class="profile-section">
  1565. <h3 class="section-title">个人资料</h3>
  1566. <el-descriptions :column="3" border>
  1567. <el-descriptions-item label="政治面貌">{{ userProfileData.profile.political_status }}</el-descriptions-item>
  1568. <el-descriptions-item label="民族">{{ userProfileData.profile.ethnicity }}</el-descriptions-item>
  1569. <el-descriptions-item label="身高">{{ userProfileData.profile.height }}cm</el-descriptions-item>
  1570. <el-descriptions-item label="体重">{{ userProfileData.profile.weight }}kg</el-descriptions-item>
  1571. <el-descriptions-item label="籍贯">{{ userProfileData.profile.native_place }}</el-descriptions-item>
  1572. <el-descriptions-item label="户口所在地">{{ userProfileData.profile.household_location }}</el-descriptions-item>
  1573. <el-descriptions-item label="现居地址">{{ userProfileData.profile.current_address }}</el-descriptions-item>
  1574. <el-descriptions-item label="婚姻状况">{{ userProfileData.profile.marital_status_name }}</el-descriptions-item>
  1575. <el-descriptions-item label="是否有子女">{{ userProfileData.profile.has_children ? '是' : '否' }}</el-descriptions-item>
  1576. <el-descriptions-item label="期望薪资">{{ userProfileData.profile.expected_salary }}</el-descriptions-item>
  1577. <el-descriptions-item label="紧急联系人">{{ userProfileData.profile.emergency_contact }}</el-descriptions-item>
  1578. <el-descriptions-item label="紧急联系电话">{{ userProfileData.profile.emergency_phone }}</el-descriptions-item>
  1579. <el-descriptions-item label="特长">{{ userProfileData.profile.specialties }}</el-descriptions-item>
  1580. <el-descriptions-item label="人生格言">{{ userProfileData.profile.life_motto }}</el-descriptions-item>
  1581. <el-descriptions-item label="招聘来源">{{ userProfileData.profile.recruitment_source_name }}</el-descriptions-item>
  1582. </el-descriptions>
  1583. </div>
  1584. <!-- 教育经历 -->
  1585. <div class="profile-section" v-if="userProfileData.educations && userProfileData.educations.length > 0">
  1586. <h3 class="section-title">教育经历</h3>
  1587. <div v-for="(edu, index) in userProfileData.educations" :key="`edu-${index}`" class="mb-4">
  1588. <el-descriptions :column="3" border>
  1589. <el-descriptions-item label="学历类型">{{ edu.education_type_name }}</el-descriptions-item>
  1590. <el-descriptions-item label="学历">{{ edu.degree_name }}</el-descriptions-item>
  1591. <el-descriptions-item label="时间段">{{ formatDate(edu.start_date) }} 至 {{ formatDate(edu.end_date) }}</el-descriptions-item>
  1592. <el-descriptions-item label="学校名称">{{ edu.school_name }}</el-descriptions-item>
  1593. <el-descriptions-item label="专业">{{ edu.major }}</el-descriptions-item>
  1594. </el-descriptions>
  1595. </div>
  1596. </div>
  1597. <!-- 工作经历 -->
  1598. <div class="profile-section" v-if="userProfileData.work_experiences && userProfileData.work_experiences.length > 0">
  1599. <h3 class="section-title">工作经历</h3>
  1600. <div v-for="(work, index) in userProfileData.work_experiences" :key="`work-${index}`" class="mb-4">
  1601. <el-descriptions :column="3" border>
  1602. <el-descriptions-item label="公司名称">{{ work.company_name }}</el-descriptions-item>
  1603. <el-descriptions-item label="时间段">{{ formatDate(work.start_date) }} 至 {{ formatDate(work.end_date) }}</el-descriptions-item>
  1604. <el-descriptions-item label="部门">{{ work.department }}</el-descriptions-item>
  1605. <el-descriptions-item label="职位">{{ work.position }}</el-descriptions-item>
  1606. <el-descriptions-item label="公司规模">{{ work.company_size }}人</el-descriptions-item>
  1607. <el-descriptions-item label="月薪">{{ work.monthly_salary }}</el-descriptions-item>
  1608. <el-descriptions-item label="主管姓名">{{ work.supervisor_name }}</el-descriptions-item>
  1609. <el-descriptions-item label="主管电话">{{ work.supervisor_phone }}</el-descriptions-item>
  1610. <el-descriptions-item label="工作描述" :span="3">{{ work.job_description || '无' }}</el-descriptions-item>
  1611. <el-descriptions-item label="工作成就" :span="3">{{ work.achievements || '无' }}</el-descriptions-item>
  1612. <el-descriptions-item label="离职原因" :span="3">{{ work.resignation_reason || '无' }}</el-descriptions-item>
  1613. </el-descriptions>
  1614. </div>
  1615. </div>
  1616. <!-- 培训经历 -->
  1617. <div class="profile-section" v-if="userProfileData.trainings && userProfileData.trainings.length > 0">
  1618. <h3 class="section-title">培训经历</h3>
  1619. <div v-for="(training, index) in userProfileData.trainings" :key="`training-${index}`" class="mb-4">
  1620. <el-descriptions :column="3" border>
  1621. <el-descriptions-item label="培训名称">{{ training.training_name }}</el-descriptions-item>
  1622. <el-descriptions-item label="培训机构">{{ training.institution }}</el-descriptions-item>
  1623. <el-descriptions-item label="时间段">{{ formatDate(training.start_date) }} 至 {{ formatDate(training.end_date) }}</el-descriptions-item>
  1624. <el-descriptions-item label="培训描述" :span="3">{{ training.description }}</el-descriptions-item>
  1625. <el-descriptions-item label="证书">{{ training.certificate }}</el-descriptions-item>
  1626. </el-descriptions>
  1627. </div>
  1628. </div>
  1629. <!-- 家庭成员 -->
  1630. <div class="profile-section" v-if="userProfileData.family_members && userProfileData.family_members.length > 0">
  1631. <h3 class="section-title">家庭成员</h3>
  1632. <div v-for="(member, index) in userProfileData.family_members" :key="`family-${index}`" class="mb-4">
  1633. <el-descriptions :column="3" border>
  1634. <el-descriptions-item label="关系">{{ member.relation }}</el-descriptions-item>
  1635. <el-descriptions-item label="姓名">{{ member.name }}</el-descriptions-item>
  1636. <el-descriptions-item label="联系电话">{{ member.phone }}</el-descriptions-item>
  1637. <el-descriptions-item label="工作单位">{{ member.workplace }}</el-descriptions-item>
  1638. <el-descriptions-item label="职位">{{ member.position }}</el-descriptions-item>
  1639. </el-descriptions>
  1640. </div>
  1641. </div>
  1642. </div>
  1643. <div v-else class="empty-data">
  1644. <el-empty description="暂无个人信息数据"></el-empty>
  1645. </div>
  1646. </el-spin>
  1647. </el-scrollbar>
  1648. <template #footer>
  1649. <span class="dialog-footer">
  1650. <el-button @click="userProfileDialogVisible = false">关闭</el-button>
  1651. </span>
  1652. </template>
  1653. </el-dialog>
  1654. </template>
  1655. <style scoped>
  1656. /* 修改背景渐变样式,使用蓝色小精灵的配色 */
  1657. .bg-gradient-custom {
  1658. background: linear-gradient(180deg, #0066cc 0%, #3399ff 100%);
  1659. min-height: 100vh;
  1660. width: 100%;
  1661. position: relative;
  1662. overflow-y: auto;
  1663. scrollbar-width: none;
  1664. -ms-overflow-style: none;
  1665. }
  1666. /* 修改星星背景效果,使其更符合蓝色主题 */
  1667. .bg-gradient-custom::before {
  1668. content: '';
  1669. position: absolute;
  1670. top: 0;
  1671. left: 0;
  1672. right: 0;
  1673. bottom: 0;
  1674. background-image:
  1675. radial-gradient(2px 2px at 20px 30px, #ffffff30 50%, transparent 100%),
  1676. radial-gradient(2px 2px at 40px 70px, #ffffff30 50%, transparent 100%),
  1677. radial-gradient(2px 2px at 50px 160px, #ffffff30 50%, transparent 100%),
  1678. radial-gradient(2px 2px at 90px 40px, #ffffff30 50%, transparent 100%),
  1679. radial-gradient(2px 2px at 130px 80px, #ffffff30 50%, transparent 100%),
  1680. radial-gradient(2px 2px at 160px 120px, #ffffff30 50%, transparent 100%);
  1681. background-repeat: repeat;
  1682. background-size: 200px 200px;
  1683. opacity: 0.6;
  1684. z-index: 0;
  1685. animation: twinkle 4s infinite alternate;
  1686. }
  1687. /* 添加闪烁动画效果 */
  1688. @keyframes twinkle {
  1689. 0% {
  1690. opacity: 0.4;
  1691. }
  1692. 100% {
  1693. opacity: 0.7;
  1694. }
  1695. }
  1696. /* 修改页面标题区域样式 */
  1697. :deep(.bg-\[#1d1e3a\]) {
  1698. background-color: #004d99 !important; /* 深蓝色标题背景 */
  1699. }
  1700. /* 修改按钮悬浮效果 */
  1701. .el-button--primary {
  1702. background-color: #0066cc !important;
  1703. border-color: #0066cc !important;
  1704. }
  1705. .el-button--primary:hover {
  1706. background-color: #3399ff !important;
  1707. border-color: #3399ff !important;
  1708. transform: scale(1.05);
  1709. transition: all 0.3s ease;
  1710. }
  1711. /* 修改内容区域的样式 */
  1712. .max-w-4xl {
  1713. position: relative;
  1714. overflow-y: visible;
  1715. min-height: 100%;
  1716. z-index: 1;
  1717. scrollbar-width: none;
  1718. -ms-overflow-style: none;
  1719. }
  1720. .max-w-4xl::-webkit-scrollbar {
  1721. display: none;
  1722. }
  1723. .video-container {
  1724. position: relative;
  1725. width: 100%;
  1726. max-width: 240px;
  1727. aspect-ratio: 3/4;
  1728. margin: 0 auto;
  1729. background: #000;
  1730. border-radius: 12px;
  1731. overflow: hidden;
  1732. }
  1733. .ant-form-item {
  1734. margin-bottom: 24px;
  1735. }
  1736. .fixed {
  1737. z-index: 100;
  1738. }
  1739. .ant-btn-circle {
  1740. width: 40px;
  1741. height: 40px;
  1742. padding: 0;
  1743. border-radius: 50%;
  1744. }
  1745. /* 修改滚动相关样式 */
  1746. html, body {
  1747. height: 100%;
  1748. scroll-behavior: smooth;
  1749. overflow-y: auto !important; /* 强制允许垂直滚动 */
  1750. }
  1751. /* 确保主容器可以正常滚动 */
  1752. .max-w-4xl {
  1753. position: relative;
  1754. overflow-y: visible;
  1755. min-height: 100%;
  1756. }
  1757. /* 添加以下样式确保内容可以正常滚动 */
  1758. body {
  1759. margin: 0;
  1760. padding: 0;
  1761. overflow-y: auto !important;
  1762. }
  1763. #app {
  1764. height: auto;
  1765. min-height: 100vh;
  1766. overflow: visible;
  1767. }
  1768. /* 确保滚动容器不会被阻止滚动 */
  1769. .ant-layout,
  1770. .ant-layout-content,
  1771. .el-scrollbar__wrap,
  1772. .el-scrollbar__view {
  1773. overflow-y: auto !important;
  1774. }
  1775. @media (max-width: 768px) {
  1776. .max-w-4xl {
  1777. padding: 1rem;
  1778. }
  1779. .grid-cols-3 {
  1780. grid-template-columns: 1fr;
  1781. }
  1782. .fixed {
  1783. right: 1rem;
  1784. }
  1785. }
  1786. /* 打印样式 */
  1787. @media print {
  1788. /* 打印时隐藏悬浮按钮 */
  1789. .fixed {
  1790. display: none !important;
  1791. }
  1792. /* 确保内容完整显示 */
  1793. .max-w-4xl {
  1794. max-width: 100% !important;
  1795. padding: 0 !important;
  1796. margin: 0 !important;
  1797. }
  1798. /* 调整页面边距 */
  1799. @page {
  1800. margin: 1cm;
  1801. }
  1802. /* 确保背景色和图片打印 */
  1803. * {
  1804. -webkit-print-color-adjust: exact !important;
  1805. color-adjust: exact !important;
  1806. print-color-adjust: exact !important;
  1807. }
  1808. /* 避免视频容器在打印时出现问题 */
  1809. .video-container {
  1810. page-break-inside: avoid;
  1811. break-inside: avoid;
  1812. }
  1813. /* 确保每个主要部分在新页面开始 */
  1814. h2.text-xl {
  1815. page-break-before: always;
  1816. break-before: always;
  1817. }
  1818. /* 第一个标题不需要分页 */
  1819. h2.text-xl:first-of-type {
  1820. page-break-before: avoid;
  1821. break-before: avoid;
  1822. }
  1823. .bg-gradient-custom {
  1824. background: none !important;
  1825. }
  1826. .bg-gradient-custom::before {
  1827. display: none !important;
  1828. }
  1829. }
  1830. /* 确保内容区域可以正常滚动 */
  1831. .max-w-4xl {
  1832. position: relative;
  1833. overflow-y: auto;
  1834. height: 100%;
  1835. padding-bottom: 100px; /* 为底部按钮留出空间 */
  1836. }
  1837. /* 添加用户个人信息弹窗样式 */
  1838. .user-profile-content {
  1839. padding: 0 16px;
  1840. }
  1841. .profile-section {
  1842. margin-bottom: 24px;
  1843. }
  1844. .section-title {
  1845. font-size: 18px;
  1846. font-weight: 600;
  1847. margin-bottom: 16px;
  1848. padding-bottom: 8px;
  1849. border-bottom: 1px solid #ebeef5;
  1850. color: #0066cc;
  1851. }
  1852. :deep(.el-descriptions__label) {
  1853. background-color: #f5f7fa;
  1854. font-weight: 500;
  1855. }
  1856. :deep(.el-descriptions__content) {
  1857. padding: 12px 16px;
  1858. }
  1859. .empty-data {
  1860. padding: 40px 0;
  1861. display: flex;
  1862. justify-content: center;
  1863. }
  1864. /* 确保弹窗内容可以滚动 */
  1865. :deep(.el-dialog__body) {
  1866. padding: 16px 0;
  1867. max-height: 70vh;
  1868. overflow: hidden;
  1869. }
  1870. /* 修改弹窗标题样式 */
  1871. :deep(.el-dialog__header) {
  1872. padding: 16px 20px;
  1873. background-color: #0066cc;
  1874. margin-right: 0;
  1875. }
  1876. :deep(.el-dialog__title) {
  1877. color: white;
  1878. font-weight: 600;
  1879. }
  1880. :deep(.el-dialog__headerbtn .el-dialog__close) {
  1881. color: white;
  1882. }
  1883. /* 修改弹窗底部样式 */
  1884. :deep(.el-dialog__footer) {
  1885. padding: 12px 20px;
  1886. border-top: 1px solid #ebeef5;
  1887. }
  1888. /* 添加标签样式 */
  1889. :deep(.el-tabs__nav-wrap) {
  1890. background-color: white;
  1891. border-radius: 8px;
  1892. padding: 4px;
  1893. }
  1894. :deep(.el-tabs__item) {
  1895. color: #606266;
  1896. font-size: 14px;
  1897. padding: 0 20px;
  1898. height: 36px;
  1899. line-height: 36px;
  1900. transition: all 0.3s;
  1901. }
  1902. :deep(.el-tabs__item.is-active) {
  1903. color: #0066cc;
  1904. font-weight: 500;
  1905. }
  1906. :deep(.el-tabs__active-bar) {
  1907. background-color: #0066cc;
  1908. }
  1909. /* 确保内容区域有足够的滚动空间 */
  1910. .space-y-4 > div {
  1911. scroll-margin-top: 120px; /* 增加滚动边距,避免被固定导航遮挡 */
  1912. }
  1913. /* 主要章节的滚动定位调整 */
  1914. #comprehensive-assessment,
  1915. #image-analysis,
  1916. #answer-records,
  1917. #other-info {
  1918. scroll-margin-top: 120px; /* 增加滚动边距 */
  1919. padding-top: 20px; /* 添加顶部内边距,使内容更清晰可见 */
  1920. border-top: 1px solid transparent; /* 添加透明边框,帮助滚动定位 */
  1921. }
  1922. /* 打印时隐藏固定导航 */
  1923. @media print {
  1924. .sticky-tabs {
  1925. display: none !important;
  1926. }
  1927. }
  1928. /* 确保固定导航不会干扰滚动 */
  1929. .sticky-tabs {
  1930. position: sticky;
  1931. top: -24px;
  1932. z-index: 10;
  1933. background-color: white;
  1934. padding: 10px 0;
  1935. margin-bottom: 20px;
  1936. /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
  1937. }
  1938. /* 自定义标签样式 - 匹配图片中的深色标签 */
  1939. :deep(.custom-tabs) {
  1940. background-color: #1d1e3a;
  1941. border-radius: 0;
  1942. }
  1943. :deep(.custom-tabs .el-tabs__header) {
  1944. margin: 0;
  1945. border: none;
  1946. background-color: #1d1e3a;
  1947. }
  1948. :deep(.custom-tabs .el-tabs__nav) {
  1949. border: none;
  1950. background-color: #1d1e3a;
  1951. }
  1952. :deep(.custom-tabs .el-tabs__item) {
  1953. color: white;
  1954. font-size: 14px;
  1955. padding: 0 20px;
  1956. height: 40px;
  1957. line-height: 40px;
  1958. border: none;
  1959. background-color: transparent;
  1960. }
  1961. :deep(.custom-tabs .el-tabs__item.is-active) {
  1962. color: white;
  1963. font-weight: 500;
  1964. background-color: #4e4e8b;
  1965. }
  1966. :deep(.custom-tabs .el-tabs__item:hover) {
  1967. color: white;
  1968. background-color: #4e4e8b;
  1969. }
  1970. :deep(.custom-tabs .el-tabs__nav-wrap::after) {
  1971. display: none;
  1972. }
  1973. .sticky-tabs {
  1974. position: sticky;
  1975. top: -25px;
  1976. z-index: 10;
  1977. margin-bottom: 20px;
  1978. }
  1979. .custom-tabs {
  1980. display: flex;
  1981. background-color: #1d1e3a;
  1982. width: 100%;
  1983. overflow-x: auto;
  1984. white-space: nowrap;
  1985. border-radius: 5px;
  1986. }
  1987. .tab-item {
  1988. padding: 0 20px;
  1989. height: 40px;
  1990. line-height: 40px;
  1991. color: white;
  1992. font-size: 14px;
  1993. cursor: pointer;
  1994. text-align: center;
  1995. transition: background-color 0.3s;
  1996. width: 25%;
  1997. /* flex: 1; */
  1998. min-width: max-content;
  1999. }
  2000. .tab-item:hover {
  2001. background-color: #4e4e8b;
  2002. }
  2003. .tab-item.active {
  2004. background-color: #4e4e8b;
  2005. font-weight: 500;
  2006. }
  2007. /* 确保在移动设备上也能正常滚动 */
  2008. @media (max-width: 768px) {
  2009. .custom-tabs {
  2010. justify-content: flex-start;
  2011. }
  2012. .tab-item {
  2013. flex: 0 0 auto;
  2014. }
  2015. }
  2016. </style>