修改模拟面试的相关内容
This commit is contained in:
64
package-lock.json
generated
64
package-lock.json
generated
@@ -12,6 +12,8 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.1",
|
"element-plus": "^2.11.1",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
@@ -1095,6 +1097,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/async-validator": {
|
"node_modules/async-validator": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||||
@@ -1491,6 +1499,24 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.11.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
@@ -1523,6 +1549,23 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||||
|
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"linkify-it": "^5.0.0",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1532,6 +1575,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
@@ -1642,6 +1691,15 @@
|
|||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.49.0",
|
"version": "4.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz",
|
||||||
@@ -1714,6 +1772,12 @@
|
|||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.1",
|
"element-plus": "^2.11.1",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus';
|
|||||||
|
|
||||||
// Create an Axios instance with a base configuration
|
// Create an Axios instance with a base configuration
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: 'http://interview.qingqiu.online/api',
|
||||||
timeout: 600000, // 10 min timeout
|
timeout: 600000, // 10 min timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
src/api/interview-message.js
Normal file
12
src/api/interview-message.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import apiClient from "@/api/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话消息列表
|
||||||
|
* @param sessionId
|
||||||
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
|
*/
|
||||||
|
export const getMessageListBySessionId = (sessionId) => {
|
||||||
|
return apiClient.get(
|
||||||
|
`/interview-message/list-by-session-id/${sessionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,12 +14,34 @@ export const startInterview = (formData) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 继续面试(发送回答)
|
* 结束面试
|
||||||
* @param {object} data - 包含sessionId和userAnswer的数据
|
* @param sessionId
|
||||||
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
export const continueInterview = (data) => {
|
export const endInterview = (sessionId) => {
|
||||||
return apiClient.post('/interview/chat', data);
|
return apiClient.post(
|
||||||
};
|
`/interview/${sessionId}/end`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交答案
|
||||||
|
* @param data
|
||||||
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
|
*/
|
||||||
|
export const submitAnswer = (data) => {
|
||||||
|
return apiClient.post('/interview/submit-answer', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一道题
|
||||||
|
* @param sessionId 会话id
|
||||||
|
* @param progressId 进度id
|
||||||
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
|
*/
|
||||||
|
export const getNextQuestion = (sessionId, progressId) => {
|
||||||
|
return apiClient.get(`/interview/next-question/${sessionId}/${progressId}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取面试历史列表
|
* 获取面试历史列表
|
||||||
@@ -33,5 +55,5 @@ export const getInterviewHistoryList = () => {
|
|||||||
* @param {string} sessionId - 面试会话ID
|
* @param {string} sessionId - 面试会话ID
|
||||||
*/
|
*/
|
||||||
export const getInterviewReportDetail = (sessionId) => {
|
export const getInterviewReportDetail = (sessionId) => {
|
||||||
return apiClient.post('/interview/get-report-detail', { sessionId });
|
return apiClient.post(`/interview/get-report-detail/${sessionId}`);
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<el-button v-if="session.status === 'COMPLETED'" type="primary" plain
|
<el-button v-if="session.status === 'COMPLETED'" type="primary" plain
|
||||||
@click="viewReport(session.sessionId)">查看复盘报告</el-button>
|
@click="viewReport(session.sessionId)">查看复盘报告</el-button>
|
||||||
<el-button v-else type="primary"
|
<el-button v-else type="primary"
|
||||||
@click="$router.push({ path: '/interview', query: { sessionId: session.sessionId } })">继续答题</el-button>
|
@click="continueInterview(session)">继续答题</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -91,6 +91,17 @@ const viewReport = (sessionId) => {
|
|||||||
router.push({ name: 'InterviewReport', params: { sessionId } });
|
router.push({ name: 'InterviewReport', params: { sessionId } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const continueInterview = (data) => {
|
||||||
|
console.log(data)
|
||||||
|
router.push({
|
||||||
|
path: '/interview-chat',
|
||||||
|
query: {
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
mode: data.model
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,74 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="report-container">
|
<div class="report-container">
|
||||||
<!-- 加载中的骨架屏 -->
|
|
||||||
<div v-if="isLoading" class="loading-container">
|
<div v-if="isLoading" class="loading-container">
|
||||||
<el-skeleton :rows="10" animated/>
|
<el-skeleton :rows="10" animated/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 无数据时的空状态 -->
|
|
||||||
<div v-else-if="!reportData" class="empty-container">
|
<div v-else-if="!reportData" class="empty-container">
|
||||||
<el-empty description="无法加载面试报告,请返回重试。"/>
|
<el-empty description="无法加载面试报告,请返回重试。"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 报告主内容 -->
|
<div v-else class="report-main">
|
||||||
<div v-else>
|
|
||||||
<!-- 报告头部 -->
|
|
||||||
<el-page-header @back="goBack" class="report-header">
|
<el-page-header @back="goBack" class="report-header">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<span class="title">{{ reportData.sessionDetails.candidateName }} 的面试复盘报告</span>
|
<span class="title">{{ reportData.sessionDetails.candidateName }} 的面试复盘报告</span>
|
||||||
<el-tag size="large">{{ new Date(reportData.sessionDetails.createdTime).toLocaleString() }}</el-tag>
|
<el-tag type="info" size="large">{{
|
||||||
|
new Date(reportData.sessionDetails.createdTime).toLocaleString()
|
||||||
|
}}
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-page-header>
|
</el-page-header>
|
||||||
|
|
||||||
<!-- AI最终评估报告 -->
|
<el-card class="box-card report-summary" shadow="hover">
|
||||||
<el-card class="box-card report-summary" shadow="never">
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<el-icon><DataAnalysis /></el-icon>
|
<el-icon>
|
||||||
|
<DataAnalysis/>
|
||||||
|
</el-icon>
|
||||||
<span>AI 最终评估报告</span>
|
<span>AI 最终评估报告</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="finalReport" class="summary-content">
|
<div v-if="finalReport" class="summary-content">
|
||||||
<el-row :gutter="20">
|
<el-descriptions :column="2" border>
|
||||||
<el-col :span="8">
|
<el-descriptions-item label="综合得分">
|
||||||
<el-statistic title="综合得分" :value="finalReport.overallScore" />
|
<el-tag size="medium">{{ finalReport.overallScore }} 分</el-tag>
|
||||||
</el-col>
|
</el-descriptions-item>
|
||||||
<el-col :span="16">
|
<el-descriptions-item label="录用建议">
|
||||||
<el-statistic title="录用建议">
|
<el-tag :type="getRecommendationType(finalReport.hiringRecommendation)" size="medium" effect="dark">
|
||||||
<template #formatter>
|
{{ finalReport.hiringRecommendation }}
|
||||||
<el-tag :type="getRecommendationType(finalReport.hiringRecommendation)" size="large" effect="dark">{{ finalReport.hiringRecommendation }}</el-tag>
|
</el-tag>
|
||||||
</template>
|
</el-descriptions-item>
|
||||||
</el-statistic>
|
</el-descriptions>
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-divider/>
|
<el-divider/>
|
||||||
<h4>综合评语</h4>
|
<h4>综合评语</h4>
|
||||||
<p class="feedback-paragraph">{{ finalReport.overallFeedback }}</p>
|
<p class="feedback-paragraph">{{ finalReport.overallFeedback }}</p>
|
||||||
<h4>技术能力评估</h4>
|
<h4>技术能力评估</h4>
|
||||||
<ul class="assessment-list">
|
<ul class="assessment-list">
|
||||||
<li v-for="(value, key) in finalReport.technicalAssessment" :key="key"><strong>{{ key }}:</strong> {{ value }}</li>
|
<li v-for="(value, key) in finalReport.technicalAssessment" :key="key">
|
||||||
|
<strong>{{ key }}:</strong> {{ value }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h4>改进建议</h4>
|
<h4>改进建议</h4>
|
||||||
<ol class="suggestions-list">
|
<ol class="suggestions-list">
|
||||||
<li v-for="suggestion in finalReport.suggestions" :key="suggestion">{{ suggestion }}</li>
|
<li v-for="suggestion in finalReport.suggestions" :key="suggestion">{{ suggestion }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div v-else><el-empty description="AI最终报告正在生成中或生成失败。" /></div>
|
<div v-else>
|
||||||
|
<el-empty description="AI最终报告正在生成中或生成失败。"/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 问答详情 -->
|
<el-card class="box-card question-details" shadow="hover">
|
||||||
<el-card class="box-card question-details" shadow="never">
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<el-icon>
|
||||||
|
<ChatDotRound/>
|
||||||
|
</el-icon>
|
||||||
<span>问答详情与逐题评估</span>
|
<span>问答详情与逐题评估</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-timeline>
|
<el-timeline>
|
||||||
<el-timeline-item v-for="(item, index) in reportData.questionDetails" :key="item.questionId" :timestamp="`第 ${index + 1} 题`" placement="top">
|
<el-timeline-item v-for="(item, index) in reportData.questionDetails" :key="item.questionId"
|
||||||
<el-card class="question-card" shadow="hover">
|
:timestamp="`第 ${index + 1} 题`" placement="top">
|
||||||
|
<el-card class="question-card" shadow="never">
|
||||||
<h4>{{ item.questionContent }}</h4>
|
<h4>{{ item.questionContent }}</h4>
|
||||||
<p class="user-answer"><strong>您的回答:</strong> {{ item.userAnswer }}</p>
|
<p class="user-answer"><strong>您的回答:</strong> {{ item.userAnswer }}</p>
|
||||||
<el-divider/>
|
<el-divider/>
|
||||||
@@ -76,7 +80,9 @@
|
|||||||
<p><strong>AI 评语:</strong> {{ item.aiFeedback }}</p>
|
<p><strong>AI 评语:</strong> {{ item.aiFeedback }}</p>
|
||||||
<p><strong>AI 建议:</strong> {{ item.suggestions }}</p>
|
<p><strong>AI 建议:</strong> {{ item.suggestions }}</p>
|
||||||
<div class="score-section">
|
<div class="score-section">
|
||||||
<strong>本题得分:</strong> <el-rate v-model="item.score" :max="5" disabled show-score text-color="#ff9900" score-template="{value} 分" />
|
<strong>本题得分:</strong>
|
||||||
|
<el-rate v-model="item.score" :max="5" disabled show-score text-color="#ff9900"
|
||||||
|
score-template="{value} 分"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -88,7 +94,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// 导入Vue核心功能、路由、API客户端和图标
|
|
||||||
import {ref, onMounted, computed} from 'vue';
|
import {ref, onMounted, computed} from 'vue';
|
||||||
import {useRouter} from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
import {getInterviewReportDetail} from '@/api/interview.js';
|
import {getInterviewReportDetail} from '@/api/interview.js';
|
||||||
@@ -99,16 +104,17 @@ const props = defineProps({ sessionId: { type: String, required: true } });
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// --- 响应式状态定义 ---
|
// --- 响应式状态定义 ---
|
||||||
const reportData = ref(null); // 存储完整的报告数据
|
const reportData = ref(null);
|
||||||
const isLoading = ref(false); // 加载状态
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
|
|
||||||
// 安全地解析最终报告的JSON字符串
|
|
||||||
const finalReport = computed(() => {
|
const finalReport = computed(() => {
|
||||||
if (reportData.value && reportData.value.sessionDetails.finalReport) {
|
if (reportData.value && reportData.value.sessionDetails.finalReport) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(reportData.value.sessionDetails.finalReport);
|
// 检查 finalReport 是否是字符串,如果是则解析
|
||||||
|
return typeof reportData.value.sessionDetails.finalReport === 'string'
|
||||||
|
? JSON.parse(reportData.value.sessionDetails.finalReport)
|
||||||
|
: reportData.value.sessionDetails.finalReport;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析最终报告JSON失败:', e);
|
console.error('解析最终报告JSON失败:', e);
|
||||||
return null;
|
return null;
|
||||||
@@ -118,14 +124,18 @@ const finalReport = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- API交互方法 ---
|
// --- API交互方法 ---
|
||||||
|
|
||||||
// 获取面试报告详情
|
|
||||||
const fetchReport = async () => {
|
const fetchReport = async () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const responseData = await getInterviewReportDetail(props.sessionId);
|
const responseData = await getInterviewReportDetail(props.sessionId);
|
||||||
|
if (responseData.code === 0 && responseData.data) {
|
||||||
reportData.value = responseData.data;
|
reportData.value = responseData.data;
|
||||||
|
} else {
|
||||||
|
reportData.value = null;
|
||||||
|
console.error('获取面试报告失败:', responseData.message);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
reportData.value = null;
|
||||||
console.error('获取面试报告失败:', error);
|
console.error('获取面试报告失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@@ -133,11 +143,8 @@ const fetchReport = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- 事件处理 ---
|
// --- 事件处理 ---
|
||||||
|
|
||||||
// 返回上一页
|
|
||||||
const goBack = () => router.push('/history');
|
const goBack = () => router.push('/history');
|
||||||
|
|
||||||
// 根据录用建议返回不同的标签类型
|
|
||||||
const getRecommendationType = (rec) => {
|
const getRecommendationType = (rec) => {
|
||||||
if (rec === '强烈推荐' || rec === '推荐') return 'success';
|
if (rec === '强烈推荐' || rec === '推荐') return 'success';
|
||||||
if (rec === '待考虑') return 'warning';
|
if (rec === '待考虑') return 'warning';
|
||||||
@@ -146,31 +153,121 @@ const getRecommendationType = (rec) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchReport();
|
fetchReport();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.report-container { padding: 10px; }
|
.report-container {
|
||||||
.report-header { margin-bottom: 20px; background-color: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); }
|
padding: 20px;
|
||||||
.header-content { display: flex; align-items: center; justify-content: space-between; width: 100%; }
|
max-width: 1200px;
|
||||||
.header-content .title { font-size: 1.2em; font-weight: 600; }
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.box-card { margin-bottom: 20px; border: none; }
|
.report-header {
|
||||||
.card-header { font-size: 1.1em; font-weight: bold; display: flex; align-items: center; }
|
margin-bottom: 20px;
|
||||||
.card-header .el-icon { margin-right: 10px; }
|
background-color: #ffffff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-content { padding: 10px; }
|
.header-content {
|
||||||
.report-summary h4 { margin: 25px 0 10px 0; font-size: 1.05em; }
|
display: flex;
|
||||||
.report-summary p, .report-summary li { color: #606266; line-height: 1.8; }
|
align-items: center;
|
||||||
.feedback-paragraph { text-indent: 2em; }
|
justify-content: space-between;
|
||||||
.assessment-list, .suggestions-list { padding-left: 20px; }
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.question-card { margin-top: 10px; }
|
.header-content .title {
|
||||||
.user-answer { color: #303133; font-style: italic; }
|
font-size: 1.5em;
|
||||||
.feedback-section { background-color: #f9fafb; padding: 15px; border-radius: 4px; margin-top: 15px; }
|
font-weight: bold;
|
||||||
.score-section { display: flex; align-items: center; margin-top: 10px; }
|
color: #303133;
|
||||||
.el-rate { margin-left: 10px; }
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .el-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-summary h4 {
|
||||||
|
margin: 25px 0 10px 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #303133;
|
||||||
|
border-left: 4px solid #409eff;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-paragraph {
|
||||||
|
text-indent: 2em;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-list, .suggestions-list {
|
||||||
|
padding-left: 20px;
|
||||||
|
list-style: disc;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-list li, .suggestions-list li {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-answer {
|
||||||
|
color: #606266;
|
||||||
|
font-style: italic;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-section {
|
||||||
|
background-color: #fafbfd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-rate {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,53 +1,491 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="interview-chat-container">
|
<div class="chat-window-container">
|
||||||
<ChatWindow
|
<div class="chat-header">
|
||||||
:mode="interviewMode"
|
<div class="header-left">
|
||||||
:categories="selectedCategories"
|
<h2>{{ pageTitle }}</h2>
|
||||||
:candidate-name="candidateName"
|
<span class="status-indicator" :class="interviewStatus"></span>
|
||||||
:session-id="sessionId"
|
<span class="status-text">{{ statusText }}</span>
|
||||||
@end-interview="handleInterviewEnd"
|
</div>
|
||||||
/>
|
<div class="header-right">
|
||||||
|
<el-button v-if="mode !== 'chat' && interviewStatus !== 'COMPLETED'" @click="interviewEnd" size="small">
|
||||||
|
结束面试
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="$emit('close')" size="small">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="messages-container" ref="messagesContainer">
|
||||||
|
<div
|
||||||
|
v-for="(message, index) in messages"
|
||||||
|
:key="index"
|
||||||
|
class="message-row"
|
||||||
|
:class="'message-' + message.sender.toLowerCase()"
|
||||||
|
>
|
||||||
|
<el-avatar
|
||||||
|
class="avatar"
|
||||||
|
:style="{ backgroundColor: message.sender === 'AI' ? '#409EFF' : '#67C23A' }"
|
||||||
|
>
|
||||||
|
{{ message.sender === 'AI' ? 'AI' : '我' }}
|
||||||
|
</el-avatar>
|
||||||
|
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-bubble" v-html="formatMessage(message.content)"></div>
|
||||||
|
<div class="message-time">{{ message.createdTime }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isAiThinking" class="message-row message-ai">
|
||||||
|
<el-avatar class="avatar" style="background-color: #409EFF;">AI</el-avatar>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-bubble thinking">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-area">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
v-model="userAnswer"
|
||||||
|
placeholder="在此输入您的回答..."
|
||||||
|
:disabled="isLoading || interviewStatus === 'COMPLETED'"
|
||||||
|
resize="none"
|
||||||
|
class="answer-input"
|
||||||
|
@keypress.enter.prevent="sendMessage"
|
||||||
|
></el-input>
|
||||||
|
|
||||||
|
<div class="input-actions">
|
||||||
|
<span class="char-counter">{{ userAnswer.length }} / 1000</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="sendMessage"
|
||||||
|
:loading="isLoading"
|
||||||
|
:disabled="interviewStatus === 'COMPLETED' || !userAnswer.trim()"
|
||||||
|
class="send-button"
|
||||||
|
>
|
||||||
|
发送回答
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="interviewStatus === 'COMPLETED'" class="completion-overlay">
|
||||||
|
<div class="completion-content">
|
||||||
|
<el-result
|
||||||
|
icon="success"
|
||||||
|
title="面试已完成"
|
||||||
|
sub-title="感谢您的参与!您可以查看面试报告或开始新的面试。"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="$router.push('/')">返回</el-button>
|
||||||
|
<el-button @click="$router.push('/history')">查看报告</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import {ref, onMounted, nextTick, computed} from 'vue'
|
||||||
|
import {ElMessage} from 'element-plus'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import ChatWindow from '@/components/ChatWindow.vue'
|
import {getMessageListBySessionId} from "@/api/interview-message.js";
|
||||||
|
import {endInterview, getNextQuestion, submitAnswer} from "@/api/interview.js";
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import hljs from 'highlight.js' // 导入 highlight.js
|
||||||
|
import 'highlight.js/styles/github.css' // 或者您喜欢的任何主题
|
||||||
|
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
breaks: true,
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return '<pre class="hljs"><code>' +
|
||||||
|
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
|
||||||
|
'</code></pre>';
|
||||||
|
} catch (__) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 从路由中获取参数
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const interviewMode = ref('ai')
|
// 定义事件
|
||||||
const selectedCategories = ref([])
|
const emit = defineEmits(['end-interview', 'close'])
|
||||||
const candidateName = ref('')
|
|
||||||
const sessionId = ref('')
|
|
||||||
|
|
||||||
onMounted(() => {
|
// 响应式状态
|
||||||
// 从路由参数获取数据
|
const messages = ref([])
|
||||||
interviewMode.value = route.query.mode || 'ai'
|
const userAnswer = ref('')
|
||||||
sessionId.value = route.query.sessionId
|
const isLoading = ref(false)
|
||||||
|
const isAiThinking = ref(false)
|
||||||
|
const interviewStatus = ref('ACTIVE') // ACTIVE, COMPLETED
|
||||||
|
const messagesContainer = ref(null)
|
||||||
|
const questionProgressId = ref(0)
|
||||||
|
|
||||||
if (route.query.categories) {
|
// 从路由 query 中获取模式参数
|
||||||
selectedCategories.value = route.query.categories.split(',').map(id => parseInt(id))
|
const mode = route.query.mode || 'chat'
|
||||||
}
|
|
||||||
|
|
||||||
// 从本地存储获取候选人姓名
|
// 计算属性
|
||||||
const sessionData = JSON.parse(localStorage.getItem('currentSession') || '{}')
|
const pageTitle = computed(() => {
|
||||||
candidateName.value = sessionData.candidateName || '候选人'
|
if (mode === 'chat') return 'AI对话'
|
||||||
sessionId.value = Date.now().toString()
|
if (mode === 'ai') return 'AI智能面试'
|
||||||
|
if (mode === 'local') return '题库面试'
|
||||||
|
return 'AI助手'
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleInterviewEnd = () => {
|
const statusText = computed(() => {
|
||||||
// 清除会话数据
|
return interviewStatus.value === 'ACTIVE' ? '进行中' : '已结束'
|
||||||
localStorage.removeItem('currentSession')
|
})
|
||||||
// 返回面试选择页面
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 从路由 query 中获取 sessionId
|
||||||
|
const sessionId = route.query.sessionId
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
// 这里是根据 sessionId 从后端加载历史消息的逻辑
|
||||||
|
getHistoryList(sessionId)
|
||||||
|
} else {
|
||||||
|
// 如果没有 sessionId,则初始化为新会话
|
||||||
router.push('/interview')
|
router.push('/interview')
|
||||||
}
|
}
|
||||||
|
scrollToBottom()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getHistoryList = async (sessionId) => {
|
||||||
|
const res = await getMessageListBySessionId(sessionId)
|
||||||
|
if (res.code === 0) {
|
||||||
|
messages.value = res.data
|
||||||
|
if (messages.value && messages.value.length > 0) {
|
||||||
|
const filterList = messages.value.filter(item => item.sender === 'AI');
|
||||||
|
if (filterList && filterList.length > 0) {
|
||||||
|
questionProgressId.value = filterList[filterList.length - 1].questionProgressId
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取历史消息失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const sendMessage = async () => {
|
||||||
|
if (!userAnswer.value.trim() || isLoading.value) return
|
||||||
|
|
||||||
|
const userMessage = {
|
||||||
|
sender: 'USER',
|
||||||
|
content: userAnswer.value,
|
||||||
|
timestamp: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.value.push(userMessage)
|
||||||
|
const currentAnswer = userAnswer.value
|
||||||
|
userAnswer.value = ''
|
||||||
|
isAiThinking.value = true
|
||||||
|
scrollToBottom()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionId = route.query.sessionId
|
||||||
|
const answerRes = await submitAnswer(
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
progressId: questionProgressId.value,
|
||||||
|
answer: currentAnswer,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (answerRes.code === 0) {
|
||||||
|
const nextQuestionRes = await getNextQuestion(sessionId, questionProgressId.value)
|
||||||
|
if (nextQuestionRes.code === 0) {
|
||||||
|
if (!nextQuestionRes.data) {
|
||||||
|
interviewEnd()
|
||||||
|
}
|
||||||
|
questionProgressId.value = nextQuestionRes.data.questionProgressId
|
||||||
|
messages.value.push(nextQuestionRes.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('发送失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
isAiThinking.value = false
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束面试
|
||||||
|
const interviewEnd = () => {
|
||||||
|
const sessionId = route.query.sessionId
|
||||||
|
endInterview(sessionId).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
router.push('/history')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('结束面试失败: ' + res.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 实际项目中,这里会调用后端 API 结束会话
|
||||||
|
// await fetch(`/api/end-session?sessionId=${sessionId}`, { method: 'POST' });
|
||||||
|
|
||||||
|
interviewStatus.value = 'COMPLETED'
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化消息内容,将换行符替换为<br>
|
||||||
|
const formatMessage = (content) => {
|
||||||
|
// return content ? content.replace(/\n/g, '<br />') : ''
|
||||||
|
// 如果内容不为空,则返回处理后的字符串
|
||||||
|
return content ? md.render(content) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.interview-chat-container {
|
/* 容器和头部样式保持不变 */
|
||||||
height: 100vh;
|
.chat-window-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: linear-gradient(135deg, #409EFF 0%, #64b5ff 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.COMPLETED {
|
||||||
|
background: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息区域 */
|
||||||
|
.messages-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-ai {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-user .avatar {
|
||||||
|
order: 2;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: calc(100% - 52px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-wrap: break-word;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-ai .message-bubble {
|
||||||
|
background-color: white;
|
||||||
|
color: #303133;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-user .message-bubble {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: white;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-ai .message-time {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-user .message-time {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入区域 */
|
||||||
|
.input-area {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #e6e8eb;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-counter {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI思考动画 */
|
||||||
|
.thinking span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #909399;
|
||||||
|
margin: 0 2px;
|
||||||
|
animation: thinking-dots 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking span:nth-child(1) {
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking span:nth-child(2) {
|
||||||
|
animation-delay: -0.16s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes thinking-dots {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 完成面试覆盖层 */
|
||||||
|
.completion-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message-row {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options-container">
|
<div class="options-container">
|
||||||
|
<!--
|
||||||
<el-card class="option-card" shadow="hover" @click="navigateTo('chat')">
|
<el-card class="option-card" shadow="hover" @click="navigateTo('chat')">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-icon size="48" color="#409EFF">
|
<el-icon size="48" color="#409EFF">
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
<el-button type="primary" class="action-btn">开始对话</el-button>
|
<el-button type="primary" class="action-btn">开始对话</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
-->
|
||||||
<el-card class="option-card" shadow="hover" @click="navigateTo('interview')">
|
<el-card class="option-card" shadow="hover" @click="navigateTo('interview')">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-icon size="48" color="#67C23A">
|
<el-icon size="48" color="#67C23A">
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ import {useRouter} from 'vue-router'
|
|||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
import {Cpu, Collection, Check} from '@element-plus/icons-vue'
|
import {Cpu, Collection, Check} from '@element-plus/icons-vue'
|
||||||
import QuestionBankSection from '@/components/QuestionBankSection.vue'
|
import QuestionBankSection from '@/components/QuestionBankSection.vue'
|
||||||
import {startInterview, continueInterview, getInterviewReportDetail} from '@/api/interview.js';
|
import {startInterview} from '@/api/interview.js';
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -183,10 +183,7 @@ const startInterviewAction = async () => {
|
|||||||
}
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const sendFormData = new FormData();
|
const sendFormData = new FormData();
|
||||||
const selectionResult = questionBankSectionRef.value.getSelectionResult()
|
|
||||||
if (!selectionResult.selectedNodes) {
|
|
||||||
selectionResult.selectedNodes = []
|
|
||||||
}
|
|
||||||
const sendData = {
|
const sendData = {
|
||||||
candidateName: formData.value.candidateName,
|
candidateName: formData.value.candidateName,
|
||||||
aiModel: formData.value.aiModel,
|
aiModel: formData.value.aiModel,
|
||||||
@@ -194,6 +191,11 @@ const startInterviewAction = async () => {
|
|||||||
model: selectedMode.value,
|
model: selectedMode.value,
|
||||||
selectedNodes: []
|
selectedNodes: []
|
||||||
}
|
}
|
||||||
|
if (selectedMode.value === 'local') {
|
||||||
|
const selectionResult = questionBankSectionRef.value.getSelectionResult()
|
||||||
|
if (!selectionResult.selectedNodes) {
|
||||||
|
selectionResult.selectedNodes = []
|
||||||
|
}
|
||||||
if (selectionResult.selectedNodes && selectionResult.selectedNodes.length > 0) {
|
if (selectionResult.selectedNodes && selectionResult.selectedNodes.length > 0) {
|
||||||
const sendNodes = []
|
const sendNodes = []
|
||||||
selectionResult.selectedNodes.forEach(node => {
|
selectionResult.selectedNodes.forEach(node => {
|
||||||
@@ -205,6 +207,7 @@ const startInterviewAction = async () => {
|
|||||||
})
|
})
|
||||||
sendData.selectedNodes = sendNodes
|
sendData.selectedNodes = sendNodes
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sendFormData.append('interviewStartDto', new Blob([JSON.stringify(sendData)], {
|
sendFormData.append('interviewStartDto', new Blob([JSON.stringify(sendData)], {
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
// proxy: {
|
||||||
// Proxy API requests to the backend server
|
// // Proxy API requests to the backend server
|
||||||
'/api': {
|
// '/api': {
|
||||||
target: 'http://localhost:8080',
|
// target: 'http://localhost:8080',
|
||||||
changeOrigin: true, // Needed for virtual hosted sites
|
// changeOrigin: true, // Needed for virtual hosted sites
|
||||||
secure: false, // Optional: if you are using https
|
// secure: false, // Optional: if you are using https
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user