Files
zhiyin/zhiyin-app/src/pages/result/result.vue
T
2026-06-16 13:18:36 +08:00

552 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view class="header">
<text class="title">{{ isOptimize ? '优化结果' : '诊断报告' }}</text>
<text class="subtitle">目标岗位{{ position }}</text>
</view>
<view v-if="loading" class="loading-wrap">
<view class="loading-spinner"></view>
<text class="loading-text">{{ isOptimize ? 'AI 正在优化简历...' : 'AI 正在诊断简历...' }}</text>
</view>
<view v-if="!loading" class="content">
<!-- 诊断模式评分 + 总结 -->
<view v-if="!isOptimize && diagnosisResult" class="section">
<view class="score-bar">
<text class="score-num">{{ diagnosisResult.score }}</text>
<text class="score-label">/100</text>
</view>
<text class="summary-text" v-if="diagnosisResult.summary">{{ diagnosisResult.summary }}</text>
</view>
<!-- 岗位匹配度诊断模式 -->
<view v-if="!isOptimize && diagnosisResult?.positionMatch" class="section">
<view class="section-title">岗位匹配度</view>
<view class="match-bar">
<view class="match-fill" :style="'width:' + diagnosisResult.positionMatch.match + '%'"></view>
</view>
<text class="match-text">{{ diagnosisResult.positionMatch.match }}% 匹配</text>
<view v-if="diagnosisResult.positionMatch.keywords?.length" class="keywords-wrap">
<text class="keywords-label">建议补充关键词</text>
<text class="keyword-tag" v-for="kw in diagnosisResult.positionMatch.keywords" :key="kw">{{ kw }}</text>
</view>
<view v-if="diagnosisResult.positionMatch.suggestions?.length" class="match-suggestions">
<text v-for="(s, i) in diagnosisResult.positionMatch.suggestions" :key="i" class="match-suggestion">{{ i+1 }}. {{ s }}</text>
</view>
</view>
<!-- 优化模式切换视图 -->
<view v-if="isOptimize" class="toggle-bar">
<text class="toggle-btn" :class="{ active: viewMode === 'optimized' }" @click="viewMode = 'optimized'">优化后</text>
<text class="toggle-btn" :class="{ active: viewMode === 'original' }" @click="viewMode = 'original'">优化前</text>
</view>
<view v-if="isOptimize" class="section">
<view class="section-title">{{ viewMode === 'optimized' ? '优化后的简历' : '原始简历' }}</view>
<view class="optimized-text">{{ viewMode === 'optimized' ? optimizedContent : originalContent }}</view>
</view>
<!-- 问题列表 / 修改说明 -->
<view v-if="changes && changes.length > 0" class="section">
<view class="section-title">{{ isOptimize ? '修改说明' : '问题列表' }}</view>
<view class="changes-list">
<view class="change-item" v-for="(change, index) in changes" :key="index">
<view class="change-type" v-if="change.type">{{ change.typeLabel || change.type }}</view>
<view class="change-section">{{ change.section || change.title }}</view>
<view class="change-arrow" v-if="change.before">{{ change.before }} {{ change.after }}</view>
<view class="change-desc">{{ change.reason || change.suggestion || change.description }}</view>
</view>
</view>
</view>
<!-- 优势 / 亮点 -->
<view v-if="highlights && highlights.length > 0" class="section">
<view class="section-title">{{ isOptimize ? '优化亮点' : '现有优势' }}</view>
<view class="highlights-list">
<view class="highlight-item" v-for="(h, i) in highlights" :key="i">{{ h }}</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="actions" v-if="!loading">
<button class="btn btn-download" @click="downloadResult">下载报告</button>
<button class="btn btn-save" @click="saveResult" v-if="saved" :disabled="saving">{{ saving ? '保存中...' : '已保存' }}</button>
<button class="btn btn-save" @click="saveResult" v-else :disabled="saving">{{ saving ? '保存中...' : '保存到我的' }}</button>
<button class="btn btn-secondary" @click="reOptimize">重新{{ isOptimize ? '优化' : '诊断' }}</button>
<button class="btn btn-primary" @click="goHome">返回首页</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import api from '../../services/api';
import { APP_CONFIG } from '../../config';
const position = ref('');
const originalContent = ref('');
const optimizedContent = ref('');
const changes = ref<any[]>([]);
const highlights = ref<string[]>([]);
const isOptimize = ref(false);
const diagnosisResult = ref<any>(null);
const viewMode = ref<'optimized' | 'original'>('optimized');
const loading = ref(true);
const saved = ref(false);
const saving = ref(false);
const resultId = ref('');
onLoad(async (options: any) => {
if (options.position) position.value = decodeURIComponent(options.position);
isOptimize.value = options.type === 'optimize';
originalContent.value = uni.getStorageSync('_resume_text') || '';
// 优先从 storage 读取分析结果(index 页存储的)
const savedData = uni.getStorageSync('_analysis_result');
if (savedData) {
try {
const data = JSON.parse(savedData);
applyResult(data);
uni.removeStorageSync('_analysis_result');
return;
} catch {
uni.removeStorageSync('_analysis_result');
}
}
// 兜底:直接调 API
try {
loading.value = true;
if (isOptimize.value) {
const res = await api.analyze.optimize(originalContent.value, position.value);
applyResult(res);
} else {
const res = await api.analyze.diagnosis(originalContent.value, position.value);
applyResult(res);
}
} catch (e: any) {
uni.showToast({ title: e.message || '分析失败', icon: 'none' });
} finally {
loading.value = false;
}
});
function applyResult(data: any) {
loading.value = false;
if (isOptimize.value) {
optimizedContent.value = data.optimized || '';
changes.value = (data.changes || []).map((c: any) =>
typeof c === 'string' ? { section: c, description: c } : c
);
highlights.value = [];
} else {
diagnosisResult.value = data;
changes.value = (data.issues || []).map((i: any) => ({
...i,
typeLabel: i.level === 'high' ? '严重' : i.level === 'medium' ? '中等' : '轻微',
description: i.desc || i.description,
}));
highlights.value = data.suggestions || [];
}
}
async function saveResult() {
if (saved.value || saving.value) return;
saving.value = true;
try {
const payload: any = {
title: (isOptimize.value ? '优化简历' : '简历诊断') + ' - ' + (position.value || '通用岗位'),
originalContent: originalContent.value,
targetPosition: position.value,
type: isOptimize.value ? 'optimize' : 'diagnosis',
};
const res = await api.resume.create(payload.title, payload.originalContent, payload.targetPosition);
resultId.value = res._id || res.id;
saved.value = true;
uni.showToast({ title: '保存成功', icon: 'success' });
} catch (e: any) {
uni.showToast({ title: e.message || '保存失败', icon: 'none' });
} finally {
saving.value = false;
}
}
function downloadResult() {
const isH5 = typeof window !== 'undefined';
let content = '';
const fileName = (isOptimize.value ? '优化简历' : '简历诊断') + '_' + (position.value || '通用岗位') + '.txt';
if (isOptimize.value) {
content = '===== 简历优化报告 =====\n\n';
content += '目标岗位:' + position.value + '\n\n';
content += '--- 优化后的简历 ---\n\n';
content += optimizedContent.value + '\n\n';
content += '--- 修改说明 ---\n\n';
changes.value.forEach((c, i) => {
content += `${i+1}. [${c.typeLabel || c.type}] ${c.section || c.title}\n`;
if (c.before) content += ` 修改前:${c.before}\n 修改后:${c.after}\n`;
content += ` 说明:${c.reason || c.suggestion || ''}\n\n`;
});
content += '--- 优化亮点 ---\n\n';
highlights.value.forEach(h => content += '• ' + h + '\n');
} else if (diagnosisResult.value) {
content = '===== 简历诊断报告 =====\n\n';
content += '目标岗位:' + position.value + '\n';
content += '综合评分:' + diagnosisResult.value.score + '/100\n\n';
content += '--- 综合评述 ---\n\n';
content += diagnosisResult.value.summary + '\n\n';
if (diagnosisResult.value.positionMatch) {
content += '--- 岗位匹配度 ---\n';
content += '匹配度:' + diagnosisResult.value.positionMatch.match + '%\n';
if (diagnosisResult.value.positionMatch.keywords?.length) {
content += '关键缺失词:' + diagnosisResult.value.positionMatch.keywords.join('、') + '\n';
}
content += '\n';
}
content += '--- 问题列表 ---\n\n';
changes.value.forEach((c, i) => {
content += `${i+1}. [${c.typeLabel || c.type}] ${c.title}\n`;
content += ` 描述:${c.description || ''}\n`;
content += ` 建议:${c.suggestion || ''}\n\n`;
});
content += '--- 现有优势 ---\n\n';
highlights.value.forEach(h => content += '• ' + h + '\n');
}
if (isH5) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
uni.showToast({ title: '下载成功', icon: 'success' });
} else {
uni.setClipboardData({
data: content,
success: () => uni.showToast({ title: '内容已复制到剪贴板' }),
});
}
}
function copyContent() {
uni.setClipboardData({
data: optimizedContent.value,
success: () => uni.showToast({ title: '已复制', icon: 'success' }),
});
}
function reOptimize() {
uni.navigateBack();
}
function goHome() {
uni.switchTab({ url: APP_CONFIG.PAGES.INDEX });
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f5f6f7;
padding: 30rpx;
padding-bottom: 160rpx;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
color: #fff;
}
.title {
font-size: 36rpx;
font-weight: 600;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 24rpx;
opacity: 0.9;
}
.loading-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid #e0e0e0;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 30rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: #999;
}
.toggle-bar {
display: flex;
background: #fff;
border-radius: 16rpx;
margin-bottom: 24rpx;
overflow: hidden;
}
.toggle-btn {
flex: 1;
text-align: center;
padding: 20rpx;
font-size: 26rpx;
color: #999;
}
.toggle-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-weight: 500;
}
.content {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.section {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.optimized-text {
font-size: 26rpx;
color: #333;
line-height: 1.8;
white-space: pre-wrap;
}
.score-bar {
text-align: center;
margin-bottom: 20rpx;
}
.score-num {
font-size: 72rpx;
font-weight: 700;
color: #ff8c00;
}
.score-label {
font-size: 28rpx;
color: #999;
}
.summary-text {
font-size: 26rpx;
color: #333;
line-height: 1.8;
}
.btn-copy {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 40rpx;
font-size: 28rpx;
margin-top: 30rpx;
}
.changes-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.change-item {
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.change-type {
display: inline-block;
padding: 4rpx 16rpx;
background: #e8f5e9;
color: #2e7d32;
border-radius: 20rpx;
font-size: 20rpx;
margin-bottom: 8rpx;
}
.change-section {
font-size: 26rpx;
font-weight: 500;
color: #667eea;
margin-bottom: 8rpx;
}
.change-arrow {
font-size: 24rpx;
color: #333;
margin-bottom: 8rpx;
}
.change-desc {
font-size: 22rpx;
color: #999;
}
.highlights-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.highlight-item {
font-size: 26rpx;
color: #333;
padding: 12rpx 20rpx;
background: #f0f8ff;
border-radius: 8rpx;
}
.match-bar {
height: 16rpx;
background: #eee;
border-radius: 8rpx;
overflow: hidden;
margin-bottom: 12rpx;
}
.match-fill {
height: 100%;
background: linear-gradient(90deg, #ff8c00, #ff6348);
border-radius: 8rpx;
transition: width 0.5s;
}
.match-text {
font-size: 24rpx;
color: #ff8c00;
font-weight: 500;
margin-bottom: 16rpx;
display: block;
}
.keywords-wrap {
margin-top: 12rpx;
}
.keywords-label {
font-size: 22rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.keyword-tag {
display: inline-block;
padding: 4rpx 16rpx;
background: #fff3e0;
color: #e65100;
border-radius: 20rpx;
font-size: 20rpx;
margin: 4rpx;
}
.match-suggestions {
margin-top: 12rpx;
}
.match-suggestion {
font-size: 22rpx;
color: #666;
display: block;
margin-bottom: 6rpx;
}
.actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
gap: 16rpx;
flex-wrap: wrap;
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06);
}
.btn {
height: 76rpx;
border-radius: 38rpx;
font-size: 26rpx;
flex: 1;
min-width: calc(50% - 8rpx);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.btn-secondary {
background: #fff;
color: #667eea;
border: 2rpx solid #667eea;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.btn-download {
background: #e8f5e9;
color: #2e7d32;
border: none;
}
.btn-save {
background: #fff8e1;
color: #f57f17;
border: none;
}
.btn-save:disabled {
opacity: 0.6;
}
</style>