优化项目,进行响应式处理

This commit is contained in:
2025-10-10 18:04:59 +08:00
parent 13615e17e4
commit 9d516642b0
12 changed files with 1656 additions and 735 deletions

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/ChatDots.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<title>AI 智能面试官 | 职位技能对话与复盘平台</title>
</head>
<body>
<div id="app"></div>

1
public/ChatDots.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760077724598" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6812" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M864 192H160A64.073143 64.073143 0 0 0 96 256V891.245714a63.890286 63.890286 0 0 0 105.179429 49.005715l127.049142-106.788572 535.771429-1.536a64.073143 64.073143 0 0 0 64-64V256a64.036571 64.036571 0 0 0-64-64z" fill="#FF861C" p-id="6813"></path><path d="M293.302857 551.899429a48.018286 48.018286 0 1 0 53.394286-79.835429 48.018286 48.018286 0 0 0-53.394286 79.835429zM485.302857 551.899429a48.018286 48.018286 0 1 0 53.394286-79.835429 48.018286 48.018286 0 0 0-53.394286 79.835429zM677.302857 551.899429a48.018286 48.018286 0 1 0 53.394286-79.835429 48.018286 48.018286 0 0 0-53.394286 79.835429z" fill="#FFD8B4" p-id="6814"></path></svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760077651596" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5557" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M274.005333 736h526.037334a64.768 64.768 0 0 0 64-64V256a64.810667 64.810667 0 0 0-64-64H224A64.810667 64.810667 0 0 0 160 256v571.008l113.962667-91.008z m22.016 64l-148.053333 118.016c-10.666667 8.021333-21.802667 9.173333-33.450667 3.498667-11.690667-5.674667-17.877333-15.189333-18.517333-28.501334V256c0.64-36.010667 13.141333-66.176 37.504-90.496 24.32-24.32 54.528-36.821333 90.496-37.504h576c36.010667 0.64 66.176 13.141333 90.496 37.504 24.32 24.32 36.821333 54.528 37.504 90.496v416c-0.64 36.010667-13.141333 66.176-37.504 90.496-24.32 24.32-54.485333 36.821333-90.496 37.504H295.978667zM512 498.986667a49.493333 49.493333 0 0 1-50.986667-50.986667A49.493333 49.493333 0 0 1 512 397.013333a49.493333 49.493333 0 0 1 50.986667 50.986667A49.493333 49.493333 0 0 1 512 498.986667z m192 0a49.493333 49.493333 0 0 1-50.986667-50.986667A49.493333 49.493333 0 0 1 704 397.013333a49.493333 49.493333 0 0 1 50.986667 50.986667 49.493333 49.493333 0 0 1-50.986667 50.986667z m-384 0A49.493333 49.493333 0 0 1 268.970667 448 49.493333 49.493333 0 0 1 320 397.013333 49.493333 49.493333 0 0 1 370.986667 448 49.493333 49.493333 0 0 1 320 498.986667z" fill="#08080A" fill-opacity=".96" p-id="5558"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,135 +0,0 @@
<template>
<div class="dashboard-container">
<!-- 欢迎横幅 -->
<el-card shadow="never" class="welcome-banner">
<div class="welcome-content">
<div class="welcome-text">
<h2>欢迎回来</h2>
<p>准备好开始您的下一次模拟面试了吗在这里管理您的题库不断提升面试技巧</p>
</div>
<img src="/src/assets/dashboard-hero.svg" alt="仪表盘插图" class="welcome-illustration" />
</div>
</el-card>
<!-- 功能导航 -->
<div class="feature-grid">
<router-link to="/interview" class="feature-card-link">
<el-card shadow="hover" class="feature-card">
<div class="card-content">
<el-icon class="card-icon" style="background-color: #ecf5ff; color: #409eff;"><ChatLineRound /></el-icon>
<div class="text-content">
<h3>开始模拟面试</h3>
<p>上传简历与AI进行实战演练</p>
</div>
</div>
</el-card>
</router-link>
<router-link to="/question-bank" class="feature-card-link">
<el-card shadow="hover" class="feature-card">
<div class="card-content">
<el-icon class="card-icon" style="background-color: #f0f9eb; color: #67c23a;"><MessageBox /></el-icon>
<div class="text-content">
<h3>题库管理</h3>
<p>新增编辑和导入您的面试题库</p>
</div>
</div>
</el-card>
</router-link>
<router-link to="/history" class="feature-card-link">
<el-card shadow="hover" class="feature-card">
<div class="card-content">
<el-icon class="card-icon" style="background-color: #fdf6ec; color: #e6a23c;"><Finished /></el-icon>
<div class="text-content">
<h3>面试历史</h3>
<p>查看过往的面试记录与AI复盘报告</p>
</div>
</div>
</el-card>
</router-link>
</div>
</div>
</template>
<script setup>
// 导入Element Plus图标
import { ChatLineRound, MessageBox, Finished } from '@element-plus/icons-vue';
</script>
<style scoped>
/* 仪表盘容器 */
.dashboard-container {
padding: 10px;
}
/* 欢迎横幅 */
.welcome-banner {
border: none;
margin-bottom: 20px;
}
.welcome-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.welcome-text h2 {
font-size: 1.8em;
margin-top: 0;
color: #303133;
}
.welcome-text p {
color: #606266;
font-size: 1.1em;
}
.welcome-illustration {
width: 200px;
height: auto;
}
/* 功能网格布局 */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.feature-card-link {
text-decoration: none;
}
.feature-card .card-content {
display: flex;
align-items: center;
padding: 20px;
transition: transform 0.3s, box-shadow 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.card-icon {
font-size: 32px;
padding: 15px;
border-radius: 50%;
margin-right: 20px;
}
.text-content h3 {
margin: 0 0 8px 0;
color: #303133;
font-size: 1.1em;
}
.text-content p {
margin: 0;
color: #909399;
font-size: 0.9em;
}
</style>

View File

@@ -1,21 +1,14 @@
<template>
<!-- 整体后台布局容器 -->
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside width="220px" class="sidebar">
<!-- Logo区域 -->
<el-aside v-if="!isMobile" width="220px" class="sidebar">
<div class="logo-container">
<el-icon class="logo-icon"><ChatDotSquare /></el-icon>
<span class="logo-title">AI面试官</span>
</div>
<!--
el-scrollbar: Element Plus的滚动条组件
通过CSS设置其高度为100%它会自动在内容溢出时显示滚动条
否则滚动条不显示解决了您提出的问题
-->
<el-scrollbar style="height: calc(100% - 60px);">
<el-menu
:default-active="$route.path"
:default-active="route.path"
background-color="#0a192f"
text-color="#8892b0"
active-text-color="#64ffda"
@@ -49,9 +42,60 @@
</el-scrollbar>
</el-aside>
<!-- 右侧主内容区 -->
<el-drawer
v-if="isMobile"
v-model="drawerVisible"
direction="ltr"
:with-header="false"
size="220px"
custom-class="mobile-sidebar-drawer"
>
<div class="logo-container">
<el-icon class="logo-icon"><ChatDotSquare /></el-icon>
<span class="logo-title">AI面试官</span>
</div>
<el-scrollbar style="height: calc(100% - 60px);">
<el-menu
:default-active="route.path"
background-color="#0a192f"
text-color="#8892b0"
active-text-color="#64ffda"
:router="true"
@select="handleMobileMenuSelect"
>
<el-menu-item index="/">
<el-icon><House /></el-icon>
<span>仪表盘</span>
</el-menu-item>
<el-menu-item index="/home">
<el-icon><ChatLineRound /></el-icon>
<span>模拟面试</span>
</el-menu-item>
<el-menu-item index="/question-bank">
<el-icon><MessageBox /></el-icon>
<span>题库管理</span>
</el-menu-item>
<el-menu-item index="/question-category">
<el-icon><MessageBox /></el-icon>
<span>题库分类</span>
</el-menu-item>
<el-menu-item index="/history">
<el-icon><Finished /></el-icon>
<span>会话历史</span>
</el-menu-item>
<el-menu-item index="/answer-record">
<el-icon><List /></el-icon>
<span>答题记录</span>
</el-menu-item>
</el-menu>
</el-scrollbar>
</el-drawer>
<el-container>
<el-header class="header">
<el-icon v-if="isMobile" class="menu-toggle-icon" @click="drawerVisible = true" size="24">
<Fold />
</el-icon>
<div class="header-title">欢迎使用AI模拟面试平台</div>
</el-header>
<el-main class="main-content">
@@ -66,7 +110,44 @@
</template>
<script setup>
import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@element-plus/icons-vue';
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import {
House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List, Fold
} from '@element-plus/icons-vue';
// 引入 useRoute 实例,用于 el-menu 的 default-active 绑定
const route = useRoute();
// 侧边栏抽屉状态(仅移动端使用)
const drawerVisible = ref(false);
// 响应式判断是否为移动端
const windowWidth = ref(window.innerWidth);
const MOBILE_WIDTH = 768; // 手机和平板的分界线
const isMobile = computed(() => windowWidth.value <= MOBILE_WIDTH);
const handleResize = () => {
windowWidth.value = window.innerWidth;
// 如果窗口宽度变大,关闭抽屉(从移动端切换回桌面端时)
if (!isMobile.value) {
drawerVisible.value = false;
}
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
// 移动端菜单选择后关闭抽屉
const handleMobileMenuSelect = () => {
drawerVisible.value = false;
};
</script>
<style scoped>
@@ -74,9 +155,9 @@ import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@
height: 100vh;
}
/* 更新后的侧边栏样式 */
/* 侧边栏样式 (仅桌面端可见,移动端使用 drawer 样式覆盖) */
.sidebar {
background-color: #0a192f; /* 更深、更现代的科技蓝 */
background-color: #0a192f;
transition: width 0.28s;
}
@@ -93,7 +174,7 @@ import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@
.logo-icon {
margin-right: 12px;
color: #64ffda; /* 高亮颜色 */
color: #64ffda;
}
.el-menu {
@@ -102,14 +183,27 @@ import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@
/* 菜单项激活时的样式 */
.el-menu-item.is-active {
background-color: #112240 !important; /* 深色背景 */
border-left: 3px solid #64ffda; /* 左侧高亮条 */
background-color: #112240 !important;
border-left: 3px solid #64ffda;
}
.el-menu-item:hover {
background-color: #112240; /* 悬浮背景色 */
background-color: #112240;
}
/* ---------------- 移动端抽屉样式优化 ---------------- */
/* 使用 :deep() 穿透作用域,修改 el-drawer 内部样式 */
:deep(.mobile-sidebar-drawer) .el-drawer__body {
padding: 0;
background-color: #0a192f; /* 确保抽屉背景色与侧边栏一致 */
}
:deep(.mobile-sidebar-drawer) .el-drawer__wrapper {
/* 增加一个半透明的蒙层 */
background-color: rgba(0, 0, 0, 0.5);
}
/* ---------------- 移动端抽屉样式优化 ---------------- */
.header {
display: flex;
align-items: center;
@@ -118,6 +212,13 @@ import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@
padding: 0 20px;
}
/* 移动端菜单切换按钮样式 */
.menu-toggle-icon {
margin-right: 15px;
cursor: pointer;
color: #303133;
}
.header-title {
font-size: 1.1em;
color: #303133;
@@ -128,6 +229,7 @@ import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@
background-color: #f0f2f5;
}
/* 过渡动画保持不变 */
.fade-transform-enter-active,
.fade-transform-leave-active {
transition: all 0.5s;

View File

@@ -1,135 +0,0 @@
<template>
<div class="dashboard-container">
<!-- 欢迎横幅 -->
<el-card shadow="never" class="welcome-banner">
<div class="welcome-content">
<div class="welcome-text">
<h2>欢迎回来</h2>
<p>准备好开始您的下一次模拟面试了吗在这里管理您的题库不断提升面试技巧</p>
</div>
<img src="/src/assets/dashboard-hero.svg" alt="仪表盘插图" class="welcome-illustration" />
</div>
</el-card>
<!-- 功能导航 -->
<div class="feature-grid">
<router-link to="/interview" class="feature-card-link">
<el-card shadow="hover" class="feature-card">
<div class="card-content">
<el-icon class="card-icon" style="background-color: #ecf5ff; color: #409eff;"><ChatLineRound /></el-icon>
<div class="text-content">
<h3>开始模拟面试</h3>
<p>上传简历与AI进行实战演练</p>
</div>
</div>
</el-card>
</router-link>
<router-link to="/question-bank" class="feature-card-link">
<el-card shadow="hover" class="feature-card">
<div class="card-content">
<el-icon class="card-icon" style="background-color: #f0f9eb; color: #67c23a;"><MessageBox /></el-icon>
<div class="text-content">
<h3>题库管理</h3>
<p>新增编辑和导入您的面试题库</p>
</div>
</div>
</el-card>
</router-link>
<router-link to="/history" class="feature-card-link">
<el-card shadow="hover" class="feature-card">
<div class="card-content">
<el-icon class="card-icon" style="background-color: #fdf6ec; color: #e6a23c;"><Finished /></el-icon>
<div class="text-content">
<h3>面试历史</h3>
<p>查看过往的面试记录与AI复盘报告</p>
</div>
</div>
</el-card>
</router-link>
</div>
</div>
</template>
<script setup>
// 导入Element Plus图标
import { ChatLineRound, MessageBox, Finished } from '@element-plus/icons-vue';
</script>
<style scoped>
/* 仪表盘容器 */
.dashboard-container {
padding: 10px;
}
/* 欢迎横幅 */
.welcome-banner {
border: none;
margin-bottom: 20px;
}
.welcome-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.welcome-text h2 {
font-size: 1.8em;
margin-top: 0;
color: #303133;
}
.welcome-text p {
color: #606266;
font-size: 1.1em;
}
.welcome-illustration {
width: 200px;
height: auto;
}
/* 功能网格布局 */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.feature-card-link {
text-decoration: none;
}
.feature-card .card-content {
display: flex;
align-items: center;
padding: 20px;
transition: transform 0.3s, box-shadow 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.card-icon {
font-size: 32px;
padding: 15px;
border-radius: 50%;
margin-right: 20px;
}
.text-content h3 {
margin: 0 0 8px 0;
color: #303133;
font-size: 1.1em;
}
.text-content p {
margin: 0;
color: #909399;
font-size: 0.9em;
}
</style>

View File

@@ -1,102 +1,398 @@
<script setup>
import { onMounted, ref } from "vue";
import { pageList } from '@/api/question-progress'
<template>
<div class="answer-record-container">
<el-card class="query-card" shadow="hover">
<template #header>
<div class="card-header">
<span>答题记录查询</span>
</div>
</template>
<el-form :inline="true" class="search-form">
<el-form-item>
<el-input placeholder="请输入需要查询的题目" v-model="searchContent" style="width: 240px" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData" icon="Search" :loading="isLoading">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="data-card" shadow="hover">
<div v-if="!isMobile" class="desktop-table-view">
<el-table :data="tableData" style="width: 100%" border :max-height="tableMaxHeight" v-loading="isLoading" empty-text="暂无答题记录">
<el-table-column align="center" prop="questionContent" label="问题" width="200" show-overflow-tooltip />
<el-table-column align="center" prop="userAnswer" label="用户回答" width="200" show-overflow-tooltip />
<el-table-column align="center" prop="score" label="评分" width="80">
<template #default="scope">
<el-tag :type="getScoreTagType(scope.row.score)" effect="dark">{{ scope.row.score }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 'ACTIVE' ? 'info' : 'success'">
{{ scope.row.status === 'ACTIVE' ? '进行中' : '已完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="center" prop="createdTime" label="创建时间" width="180" />
<el-table-column align="center" label="AI分析" width="120" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="showDetails(scope.row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="mobile-card-view" v-loading="isLoading">
<el-collapse v-model="activeNames">
<el-collapse-item
v-for="(row, index) in tableData"
:key="index"
:name="index"
class="answer-card-item"
>
<template #title>
<div class="card-title-mobile">
<el-tag :type="getScoreTagType(row.score)" effect="dark" size="small">{{ row.score }}</el-tag>
<span class="q-content-text">{{ row.questionContent }}</span>
<span class="time-text">{{ formatTime(row.createdTime) }}</span>
</div>
</template>
<div class="card-detail-content">
<h4>用户回答:</h4>
<p class="answer-text">{{ row.userAnswer }}</p>
<el-divider />
<h4>AI 分析:</h4>
<el-tabs type="border-card">
<el-tab-pane label="AI 回答">
<p class="ai-analysis-text">{{ row.aiAnswer }}</p>
</el-tab-pane>
<el-tab-pane label="AI 反馈">
<p class="ai-analysis-text">{{ row.feedback }}</p>
</el-tab-pane>
<el-tab-pane label="AI 建议">
<p class="ai-analysis-text">{{ row.suggestions }}</p>
</el-tab-pane>
</el-tabs>
</div>
</el-collapse-item>
</el-collapse>
<p v-if="!tableData.length && !isLoading" class="empty-data-tip">暂无答题记录</p>
</div>
<el-pagination class="pagination-container"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:background="true"
:layout="paginationLayout"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<el-dialog v-model="dialogVisible" :title="`AI 分析详情 - 评分: ${currentRow.score}`" width="600px" custom-class="detail-dialog">
<el-tabs type="border-card">
<el-tab-pane label="问题与回答">
<div class="detail-section">
<h4 class="detail-title">问题:</h4>
<p>{{ currentRow.questionContent }}</p>
</div>
<el-divider />
<div class="detail-section">
<h4 class="detail-title">用户回答:</h4>
<p>{{ currentRow.userAnswer }}</p>
</div>
</el-tab-pane>
<el-tab-pane label="AI 回答">
<p class="analysis-content">{{ currentRow.aiAnswer }}</p>
</el-tab-pane>
<el-tab-pane label="AI 反馈">
<p class="analysis-content">{{ currentRow.feedback }}</p>
</el-tab-pane>
<el-tab-pane label="AI 建议">
<p class="analysis-content">{{ currentRow.suggestions }}</p>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, ref, computed, onBeforeUnmount } from "vue";
import { pageList } from '@/api/question-progress' // 确保路径正确
import { Search } from '@element-plus/icons-vue'
// --- 响应式状态 ---
const tableData = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const size = ref('default')
const background = ref(false)
const disabled = ref(false)
const handleSizeChange = (val) => {
console.log(`${val} items per page`)
pageSize.value = val
fetchData()
}
const total = ref(0)
const handleCurrentChange = (val) => {
console.log(`current page: ${val}`)
currentPage.value = val
fetchData()
}
const searchContent = ref('')
const isLoading = ref(false)
const dialogVisible = ref(false)
const currentRow = ref({})
const activeNames = ref([0])
// --- 响应式断点控制 ---
const windowWidth = ref(window.innerWidth)
const MOBILE_WIDTH = 768
const isMobile = computed(() => windowWidth.value <= MOBILE_WIDTH)
const tableMaxHeight = computed(() => {
return isMobile.value ? 'auto' : 'calc(100vh - 280px)'
})
const paginationLayout = computed(() => {
return isMobile.value ? 'total, prev, pager, next' : 'total, sizes, prev, pager, next, jumper'
})
// 监听窗口大小变化
const handleResize = () => {
windowWidth.value = window.innerWidth
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
// --- 数据处理和方法 ---
const fetchData = () => {
isLoading.value = true
pageList({
current: currentPage.value,
size: pageSize.value,
questionName: searchContent.value
}).then(({ data }) => {
console.log(data)
tableData.value = data.records
currentPage.value = data.current
total.value = data.total
}).catch(error => {
console.error("Fetch data failed:", error)
// 可以在这里添加一个 ElMessage 错误提示
}).finally(() => {
isLoading.value = false
})
}
const handleSizeChange = (val) => {
pageSize.value = val
fetchData()
}
const handleCurrentChange = (val) => {
currentPage.value = val
fetchData()
}
// 显示详情弹窗 (PC/平板)
const showDetails = (row) => {
currentRow.value = row
dialogVisible.value = true
}
// 根据分数判断 Tag 类型
const getScoreTagType = (score) => {
if (score >= 80) return 'success'
if (score >= 60) return 'warning'
return 'danger'
}
// 格式化时间
const formatTime = (time) => {
if (!time) return ''
const date = new Date(time)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
}
onMounted(() => {
fetchData();
});
</script>
<template>
<el-card>
<!-- 上半部分-->
<div>
<el-card style="width: 100%;margin-bottom: 15px;" shadow="hover">
<template #header>
<div class="card-header">
<span>答题记录</span>
</div>
</template>
<el-form :inline="true">
<el-form-item style="margin: 0;">
<el-input placeholder="请输入需要查询的题目" v-model="searchContent" style="width: 240px"></el-input>
</el-form-item>
<el-form-item style="margin-bottom: 0;margin-left: 15px;">
<el-button type="primary" @click="fetchData">查询</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 数据部分-->
<style scoped>
/* 基础容器 */
.answer-record-container {
padding: 20px;
background-color: #f0f2f5;
min-height: calc(100vh - 80px);
}
<el-card style="width: 100%;" shadow="hover">
<el-table :data="tableData" style="width: 100%" border height="calc(100vh - 260px)">
<el-table-column align="center" prop="questionContent" label="问题" />
<el-table-column align="center" prop="userAnswer" label="用户回答" />
<el-table-column align="center" prop="aiAnswer" label="AI回答" />
<el-table-column align="center" prop="feedback" label="AI反馈" />
<el-table-column align="center" prop="suggestions" label="AI建议" />
<el-table-column align="center" prop="score" label="AI评分" />
<el-table-column #default="scope" align="center">
<el-tag :type="scope.row.status === 'ACTIVE' ? 'primary' : 'success'">
{{ scope.row.status }}
</el-tag>
</el-table-column>
<el-table-column align="center" prop="createdTime" label="创建时间" />
</el-table>
/* 卡片样式 */
.el-card {
border-radius: 12px;
margin-bottom: 15px;
}
<el-pagination class="pagination-container" v-model:current-page="currentPage" v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]" :size="size" :disabled="disabled" :background="background"
layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</el-card>
</el-card>
</template>
.query-card {
background-color: #ffffff;
}
.data-card {
padding: 0;
}
<style scoped lang="css">
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.2em;
font-size: 1.25rem;
font-weight: 600;
color: #303133;
}
.search-form {
margin-bottom: 0 !important;
}
/* ----------------------------------------------------- */
/* PC/平板样式 (默认) */
/* ----------------------------------------------------- */
/* 表格内容长文本样式优化 */
/* 使用 :deep() 穿透作用域,调整 Element Plus 内部样式 */
:deep(.el-table .el-table__cell) {
padding: 8px 0;
}
.detail-dialog .detail-title {
font-weight: bold;
color: #303133;
margin-bottom: 5px;
}
.detail-dialog p {
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
}
.analysis-content {
max-height: 200px;
overflow-y: auto;
padding-right: 10px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
padding: 10px 0;
}
.empty-data-tip {
text-align: center;
padding: 20px;
color: #909399;
}
/* ----------------------------------------------------- */
/* 响应式优化:手机 (width <= 768px) */
/* ----------------------------------------------------- */
@media (max-width: 768px) {
.answer-record-container {
padding: 10px;
}
/* 查询表单优化 */
.search-form {
display: flex;
flex-direction: column;
align-items: stretch;
}
.search-form .el-form-item {
margin-right: 0 !important;
width: 100%;
margin-bottom: 10px;
}
.search-form .el-input {
width: 100% !important;
}
.search-form .el-button {
width: 100%;
}
/* 移动端卡片列表样式 */
.answer-card-item {
margin-bottom: 10px;
border: 1px solid #ebeef5;
border-radius: 8px;
overflow: hidden;
}
/* 移动端卡片标题 */
.card-title-mobile {
display: grid;
grid-template-columns: 50px 1fr 80px;
align-items: center;
gap: 10px;
padding: 5px 0;
width: 100%;
}
.q-content-text {
font-weight: 600;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time-text {
color: #909399;
font-size: 0.8em;
text-align: right;
}
/* 卡片详细内容 */
.card-detail-content {
padding: 0 10px 10px 10px;
}
.card-detail-content h4 {
margin: 10px 0 5px 0;
color: #409EFF;
font-size: 1rem;
}
.answer-text, .ai-analysis-text {
color: #606266;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
/* 分页优化 */
.pagination-container {
justify-content: center;
}
/* 覆盖 el-collapse 默认边框,使其看起来像卡片 */
:deep(.el-collapse) {
border-top: none;
border-bottom: none;
}
:deep(.el-collapse-item__header) {
height: auto;
line-height: normal;
padding: 10px;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
}
</style>

View File

@@ -13,7 +13,7 @@
<template #content>
<div class="header-content">
<span class="title">{{ reportData.sessionDetails.candidateName }} 的面试复盘报告</span>
<el-tag type="info" size="large">{{
<el-tag type="info" size="large" class="report-time-tag">{{
new Date(reportData.sessionDetails.createdTime).toLocaleString()
}}
</el-tag>
@@ -23,34 +23,39 @@
<el-card class="box-card report-summary" shadow="hover">
<template #header>
<div class="card-header">
<el-icon>
<div class="card-header primary-color">
<el-icon size="20">
<DataAnalysis/>
</el-icon>
<span>AI 最终评估报告</span>
</div>
</template>
<div v-if="finalReport" class="summary-content">
<el-descriptions :column="2" border>
<el-descriptions :column="isMobile ? 1 : 2" border>
<el-descriptions-item label="目标岗位">{{ reportData.sessionDetails.jobRequirements || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="面试模式">{{ reportData.sessionDetails.model === 'ai' ? 'AI智能面试' : '本地题库面试' }}</el-descriptions-item>
<el-descriptions-item label="综合得分">
<el-tag size="medium">{{ finalReport.overallScore }} </el-tag>
<el-tag size="large" type="success" effect="dark" class="score-tag">{{ finalReport.overallScore }} </el-tag>
</el-descriptions-item>
<el-descriptions-item label="录用建议">
<el-tag :type="getRecommendationType(finalReport.hiringRecommendation)" size="medium" effect="dark">
<el-tag :type="getRecommendationType(finalReport.hiringRecommendation)" size="large" effect="dark">
{{ finalReport.hiringRecommendation }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-divider/>
<h4>综合评语</h4>
<h4 class="section-title">综合评语</h4>
<p class="feedback-paragraph">{{ finalReport.overallFeedback }}</p>
<h4>技术能力评估</h4>
<h4 class="section-title">技术能力评估</h4>
<ul class="assessment-list">
<li v-for="(value, key) in finalReport.technicalAssessment" :key="key">
<strong>{{ key }}:</strong> {{ value }}
</li>
</ul>
<h4>改进建议</h4>
<h4 class="section-title">改进建议</h4>
<ol class="suggestions-list">
<li v-for="suggestion in finalReport.suggestions" :key="suggestion">{{ suggestion }}</li>
</ol>
@@ -62,23 +67,27 @@
<el-card class="box-card question-details" shadow="hover">
<template #header>
<div class="card-header">
<el-icon>
<div class="card-header success-color">
<el-icon size="20">
<ChatDotRound/>
</el-icon>
<span>问答详情与逐题评估</span>
</div>
</template>
<el-timeline>
<el-timeline class="custom-timeline">
<el-timeline-item v-for="(item, index) in reportData.questionDetails" :key="item.questionId"
:timestamp="`第 ${index + 1} 题`" placement="top">
<el-card class="question-card" shadow="never">
<h4>{{ item.questionContent }}</h4>
<p class="user-answer"><strong>您的回答:</strong> {{ item.userAnswer }}</p>
<el-divider/>
:timestamp="`第 ${index + 1} 题 - ${formatTime(item.createdTime)}`" placement="top">
<el-card class="question-card" shadow="always">
<h4 class="question-content">{{ item.questionContent }}</h4>
<el-divider content-position="left">用户回答</el-divider>
<p class="user-answer-text">{{ item.userAnswer }}</p>
<el-divider content-position="left">AI 评估</el-divider>
<div class="feedback-section">
<p><strong>AI 评语:</strong> {{ item.aiFeedback }}</p>
<p><strong>AI 建议:</strong> {{ item.suggestions }}</p>
<div class="score-section">
<strong>本题得分:</strong>
<el-rate v-model="item.score" :max="5" disabled show-score text-color="#ff9900"
@@ -94,7 +103,7 @@
</template>
<script setup>
import {ref, onMounted, computed} from 'vue';
import {ref, onMounted, computed, onBeforeUnmount} from 'vue';
import {useRouter} from 'vue-router';
import {getInterviewReportDetail} from '@/api/interview.js';
import {DataAnalysis, ChatDotRound} from '@element-plus/icons-vue';
@@ -107,14 +116,50 @@ const router = useRouter();
const reportData = ref(null);
const isLoading = ref(false);
// --- 响应式断点控制 ---
const windowWidth = ref(window.innerWidth);
const MOBILE_WIDTH = 768;
const isMobile = computed(() => windowWidth.value <= MOBILE_WIDTH);
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener('resize', handleResize);
fetchReport();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
// --- 计算属性 ---
const finalReport = computed(() => {
if (reportData.value && reportData.value.sessionDetails.finalReport) {
try {
// 检查 finalReport 是否是字符串,如果是则解析
return typeof reportData.value.sessionDetails.finalReport === 'string'
const reportJson = typeof reportData.value.sessionDetails.finalReport === 'string'
? JSON.parse(reportData.value.sessionDetails.finalReport)
: reportData.value.sessionDetails.finalReport;
// 确保 technicalAssessment 和 suggestions 字段存在且为数组/对象
if (reportJson.technicalAssessment && typeof reportJson.technicalAssessment === 'string') {
try {
reportJson.technicalAssessment = JSON.parse(reportJson.technicalAssessment);
} catch(e) {
console.warn("Technical assessment parsing failed, keeping as string.");
}
}
if (reportJson.suggestions && typeof reportJson.suggestions === 'string') {
try {
reportJson.suggestions = JSON.parse(reportJson.suggestions);
} catch(e) {
console.warn("Suggestions parsing failed, keeping as string.");
}
}
return reportJson;
} catch (e) {
console.error('解析最终报告JSON失败:', e);
return null;
@@ -142,36 +187,43 @@ const fetchReport = async () => {
}
};
// --- 事件处理 ---
// --- 事件处理/工具函数 ---
const goBack = () => router.push('/history');
const getRecommendationType = (rec) => {
if (rec === '强烈推荐' || rec === '推荐') return 'success';
if (rec === '待考虑') return 'warning';
if (rec === '不推荐') return 'danger';
if (rec && rec.includes('强烈推荐') || rec.includes('推荐')) return 'success';
if (rec && rec.includes('待考虑')) return 'warning';
if (rec && rec.includes('不推荐')) return 'danger';
return 'info';
};
// --- 生命周期钩子 ---
onMounted(() => {
fetchReport();
const formatTime = (time) => {
if (!time) return '';
return new Date(time).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<style scoped>
/* ----------------------------------------------------- */
/* PC/平板样式 (默认) */
/* ----------------------------------------------------- */
.report-container {
padding: 20px;
padding: 30px;
max-width: 1200px;
margin: 0 auto;
}
.report-header {
margin-bottom: 20px;
margin-bottom: 25px;
background-color: #ffffff;
padding: 15px 20px;
padding: 20px 30px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #ebeef5;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
border: 1px solid #e4e7ed;
}
.header-content {
@@ -182,92 +234,282 @@ onMounted(() => {
}
.header-content .title {
font-size: 1.5em;
font-weight: bold;
font-size: 1.8em;
font-weight: 700;
color: #303133;
}
.report-time-tag {
font-size: 1rem;
}
.box-card {
margin-bottom: 20px;
border: 1px solid #ebeef5;
margin-bottom: 25px;
border: 1px solid #e4e7ed;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.box-card:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.card-header {
font-size: 1.2em;
font-weight: bold;
font-size: 1.3em;
font-weight: 600;
display: flex;
align-items: center;
color: #303133;
padding: 15px 20px;
margin: -20px; /* 负边距抵消卡片内边距 */
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.card-header.primary-color {
background-color: #ecf5ff;
color: #409eff;
}
.card-header.success-color {
background-color: #f0f9eb;
color: #67c23a;
}
.card-header .el-icon {
margin-right: 10px;
color: #409eff;
}
.summary-content {
padding: 10px 0;
}
.el-descriptions {
margin-bottom: 20px;
/* Descriptions 样式优化 */
:deep(.el-descriptions__label.is-bordered-label) {
background-color: #f5f7fa;
font-weight: 600;
color: #606266;
}
.score-tag {
font-weight: bold;
animation: pulse-score 2s infinite;
}
@keyframes pulse-score {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.report-summary h4 {
margin: 25px 0 10px 0;
font-size: 1.1em;
.section-title {
margin: 30px 0 10px 0;
font-size: 1.2em;
color: #303133;
border-left: 4px solid #409eff;
padding-left: 10px;
border-left: 5px solid #67c23a;
padding-left: 15px;
font-weight: 600;
}
.feedback-paragraph {
text-indent: 2em;
color: #606266;
color: #5a646e;
line-height: 1.8;
margin-bottom: 20px;
padding: 0 10px;
}
.assessment-list, .suggestions-list {
padding-left: 20px;
list-style: disc;
color: #606266;
padding-left: 25px;
list-style-type: '👉 '; /* 趣味列表符号 */
list-style-position: inside;
color: #5a646e;
}
.suggestions-list {
list-style-type: '💡 ';
}
.assessment-list li, .suggestions-list li {
line-height: 1.8;
line-height: 2;
margin-bottom: 5px;
}
.assessment-list strong, .suggestions-list strong {
color: #303133;
}
/* 问答详情 Timeline */
.custom-timeline {
padding-left: 10px;
}
.question-card {
margin-top: 10px;
border: 1px solid #ebeef5;
border-radius: 8px;
border: 1px solid #dcdfe6;
border-radius: 10px;
transition: box-shadow 0.3s ease;
}
.question-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.user-answer {
color: #606266;
.question-content {
font-size: 1.1em;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
.user-answer-text {
color: #5a646e;
font-style: italic;
background-color: #f5f7fa;
padding: 10px;
border-radius: 4px;
background-color: #fcfcfc;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
border: 1px solid #ebeef5;
}
.feedback-section {
background-color: #fafbfd;
background-color: #ffffff;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border: 1px dashed #dcdfe6;
}
.feedback-section p {
line-height: 1.6;
color: #5a646e;
margin-bottom: 8px;
}
.feedback-section strong {
color: #303133;
font-weight: 600;
}
.score-section {
display: flex;
align-items: center;
margin-top: 10px;
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #ebeef5;
}
.el-rate {
margin-left: 10px;
margin-left: 15px;
}
/* ----------------------------------------------------- */
/* 响应式优化:手机/小屏 (width <= 768px) */
/* ----------------------------------------------------- */
@media (max-width: 768px) {
.report-container {
padding: 10px; /* 减少内边距 */
}
/* 头部优化 */
.report-header {
padding: 10px 15px;
margin-bottom: 15px;
}
.header-content {
flex-direction: column; /* 垂直堆叠标题和时间 */
align-items: flex-start;
}
.header-content .title {
font-size: 1.3em;
margin-bottom: 5px;
/* 解决 iPhone 14 Pro 标题换行问题 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 90vw; /* 限制最大宽度,防止溢出 */
}
.report-time-tag {
font-size: 0.8em;
}
/* 卡片头部 */
.box-card {
margin-bottom: 15px;
}
.card-header {
font-size: 1.1em;
padding: 10px 15px;
margin: -15px; /* 适配卡片内边距 */
}
/* 摘要内容 */
.summary-content {
padding: 0;
}
/* 强制 Descriptions 单列 */
:deep(.el-descriptions-item__container) {
display: block !important;
}
:deep(.el-descriptions-item__label.is-bordered-label) {
width: 100% !important;
padding-bottom: 5px;
padding-top: 5px;
text-align: left;
}
:deep(.el-descriptions-item__content) {
padding-bottom: 10px;
padding-top: 0;
width: 100% !important;
}
/* 评语和列表 */
.section-title {
font-size: 1.1em;
margin: 20px 0 8px 0;
padding-left: 10px;
}
.feedback-paragraph {
font-size: 0.9em;
line-height: 1.6;
text-indent: 1.5em;
}
.assessment-list, .suggestions-list {
padding-left: 15px;
font-size: 0.9em;
}
/* 问答详情 */
.custom-timeline {
padding-left: 5px;
}
:deep(.el-timeline-item__timestamp) {
font-size: 0.9em;
}
.question-content {
font-size: 1em;
}
.user-answer-text {
padding: 10px;
font-size: 0.9em;
}
.feedback-section {
padding: 10px;
font-size: 0.9em;
}
.score-section {
flex-direction: column; /* 垂直堆叠得分标签和星星 */
align-items: flex-start;
}
.el-rate {
margin-left: 0;
margin-top: 5px;
}
}
</style>

View File

@@ -7,10 +7,13 @@
<span class="status-text">{{ statusText }}</span>
</div>
<div class="header-right">
<el-button v-if="mode !== 'chat' && interviewStatus !== 'COMPLETED'" @click="interviewEnd" size="small">
<el-button v-if="mode !== 'chat' && interviewStatus !== 'COMPLETED'" @click="interviewEnd" size="small"
class="header-button end-button" type="danger" plain>
结束面试
</el-button>
<el-button @click="$emit('close')" size="small">关闭</el-button>
<el-button @click="$router.push('/home')" size="small" class="header-button close-button" type="info" plain>
关闭
</el-button>
</div>
</div>
@@ -21,24 +24,38 @@
class="message-row"
:class="'message-' + message.sender.toLowerCase()"
>
<template v-if="message.sender === 'AI'">
<el-avatar
class="avatar"
:style="{ backgroundColor: message.sender === 'AI' ? '#409EFF' : '#67C23A' }"
>
{{ message.sender === 'AI' ? 'AI' : '我' }}
</el-avatar>
style="background-color: #409EFF; margin-right: 12px;"
>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 class="message-bubble markdown-body" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ formatTime(message.createdTime) }}</div>
</div>
</template>
<template v-else>
<div class="message-content">
<div class="message-bubble markdown-body" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ formatTime(message.createdTime) }}</div>
</div>
<div v-if="isAiThinking" class="message-row message-ai">
<el-avatar class="avatar" style="background-color: #409EFF;">AI</el-avatar>
<el-avatar
class="avatar"
style="background-color: #67C23A; margin-left: 12px;"
></el-avatar>
</template>
</div>
<div v-if="isAiThinking" class="message-row message-ai typing-indicator-row">
<el-avatar class="avatar" style="background-color: #409EFF; margin-right: 12px;">AI</el-avatar>
<div class="message-content">
<div class="message-bubble thinking">
<span></span><span></span><span></span>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
</div>
@@ -49,9 +66,11 @@
type="textarea"
:rows="4"
v-model="userAnswer"
placeholder="在此输入您的回答..."
placeholder="在此输入您的回答 (Enter 键发送)..."
:disabled="isLoading || interviewStatus === 'COMPLETED'"
resize="none"
maxlength="1000"
show-word-limit
class="answer-input"
@keypress.enter.prevent="sendMessage"
></el-input>
@@ -65,6 +84,7 @@
:disabled="interviewStatus === 'COMPLETED' || !userAnswer.trim()"
class="send-button"
>
<el-icon><i class="el-icon-s-promotion"></i></el-icon>
发送回答
</el-button>
</div>
@@ -78,8 +98,8 @@
sub-title="感谢您的参与您可以查看面试报告或开始新的面试"
>
<template #extra>
<el-button type="primary" @click="$router.push('/')">返回</el-button>
<el-button @click="$router.push('/history')">查看报告</el-button>
<el-button type="primary" @click="$router.push('/')">返回主页</el-button>
<el-button @click="$router.push('/history')" type="info" plain>查看报告</el-button>
</template>
</el-result>
</div>
@@ -91,11 +111,12 @@
import {ref, onMounted, nextTick, computed} from 'vue'
import {ElMessage} from 'element-plus'
import {useRoute, useRouter} from 'vue-router'
// **保持原有接口导入,不对接口逻辑进行修改**
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' // 或者您喜欢的任何主题
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css' // 导入您选择的 highlight.js 主题
const md = new MarkdownIt({
html: true,
@@ -107,7 +128,8 @@ const md = new MarkdownIt({
return '<pre class="hljs"><code>' +
hljs.highlight(str, {language: lang, ignoreIllegals: true}).value +
'</code></pre>';
} catch (__) {}
} catch (__) {
}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
@@ -132,6 +154,7 @@ const questionProgressId = ref(0)
// 从路由 query 中获取模式参数
const mode = route.query.mode || 'chat'
const sessionId = route.query.sessionId
// 计算属性
const pageTitle = computed(() => {
@@ -142,59 +165,71 @@ const pageTitle = computed(() => {
})
const statusText = computed(() => {
return interviewStatus.value === 'ACTIVE' ? '进行中' : '已结束'
// 根据实际的 interviewStatus 更新
if (interviewStatus.value === 'COMPLETED') return '已结束'
if (interviewStatus.value === 'ACTIVE') return '进行中'
return '加载中'
})
// 生命周期钩子
onMounted(() => {
// 从路由 query 中获取 sessionId
const sessionId = route.query.sessionId
if (sessionId) {
// 这里是根据 sessionId 从后端加载历史消息的逻辑
getHistoryList(sessionId)
} else {
// 如果没有 sessionId则初始化为新会话
ElMessage.error('会话ID缺失请返回首页。')
router.push('/interview')
}
scrollToBottom()
})
const getHistoryList = async (sessionId) => {
try {
const res = await getMessageListBySessionId(sessionId)
if (res.code === 0) {
messages.value = res.data
// **保持与原始逻辑一致**
messages.value = res.data || []
// 假设后端返回的数据结构中直接包含 status 字段
if (res.data && res.data.status) {
interviewStatus.value = res.data.status
}
if (messages.value && messages.value.length > 0) {
const filterList = messages.value.filter(item => item.sender === 'AI');
if (filterList && filterList.length > 0) {
// 获取最新的 AI 消息对应的 questionProgressId
questionProgressId.value = filterList[filterList.length - 1].questionProgressId
}
}
} else {
ElMessage.error('获取历史消息失败')
}
} catch (e) {
ElMessage.error('获取历史消息失败,网络错误')
} finally {
scrollToBottom()
}
}
// 发送消息
const sendMessage = async () => {
if (!userAnswer.value.trim() || isLoading.value) return
if (!userAnswer.value.trim() || isLoading.value || interviewStatus.value === 'COMPLETED') return
const currentAnswer = userAnswer.value
// 1. 立即显示用户消息
const userMessage = {
sender: 'USER',
content: userAnswer.value,
timestamp: new Date()
content: currentAnswer,
createdTime: new Date().toISOString() // 使用当前时间作为占位符
}
messages.value.push(userMessage)
const currentAnswer = userAnswer.value
userAnswer.value = ''
isAiThinking.value = true
scrollToBottom()
try {
const sessionId = route.query.sessionId
// 2. 提交答案 (不修改接口调用逻辑)
const answerRes = await submitAnswer(
{
sessionId,
@@ -202,55 +237,69 @@ const sendMessage = async () => {
answer: currentAnswer,
}
)
// 3. 获取下一题或结束信息 (不修改接口调用逻辑)
if (answerRes.code === 0) {
const nextQuestionRes = await getNextQuestion(sessionId, questionProgressId.value)
if (nextQuestionRes.code === 0) {
if (!nextQuestionRes.data) {
interviewEnd()
}
if (!nextQuestionRes.data || nextQuestionRes.data.isComplete) {
// 明确判断是否结束
await interviewEnd(true) // 传递一个参数表示是正常流程结束
interviewStatus.value = 'COMPLETED'
} else {
// 接收新问题
questionProgressId.value = nextQuestionRes.data.questionProgressId
messages.value.push(nextQuestionRes.data)
}
} else {
ElMessage.error('获取下一题失败: ' + nextQuestionRes.message)
}
} else {
ElMessage.error('提交回答失败: ' + answerRes.message)
}
} catch (error) {
ElMessage.error('发送失败,请稍后重试。')
console.error(error)
ElMessage.error('发送失败,请检查网络或联系管理员。')
} finally {
isAiThinking.value = false
scrollToBottom()
}
}
// 结束面试
const interviewEnd = () => {
const sessionId = route.query.sessionId
endInterview(sessionId).then((res) => {
// 结束面试 (不修改接口调用逻辑)
const interviewEnd = async (isAutoCompleted = false) => {
if (interviewStatus.value === 'COMPLETED') return;
try {
const res = await endInterview(sessionId)
if (res.code === 0) {
router.push('/history')
interviewStatus.value = 'COMPLETED'
if (!isAutoCompleted) {
ElMessage.success('面试已成功结束,即将为您生成报告。')
}
scrollToBottom()
} else {
ElMessage.error('结束面试失败: ' + res.message)
}
})
// 实际项目中,这里会调用后端 API 结束会话
// await fetch(`/api/end-session?sessionId=${sessionId}`, { method: 'POST' });
interviewStatus.value = 'COMPLETED'
scrollToBottom()
} catch (e) {
ElMessage.error('结束面试失败,网络错误。')
}
}
// 格式化消息内容,将换行符替换为<br>
// 格式化消息内容,使用 MarkdownIt 渲染
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', {
if (!timestamp) return ''
// 确保 timestamp 是一个 Date 对象或可以被 Date 构造函数解析
const date = new Date(timestamp);
if (isNaN(date.getTime())) return ''; // 如果时间无效,返回空字符串
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
@@ -267,25 +316,43 @@ const scrollToBottom = () => {
</script>
<style scoped>
/* 容器和头部样式保持不变 */
/* 默认样式PC/平板 (大于 768px) */
:root {
--primary-color: #409EFF; /* Element Plus 主题蓝 */
--ai-bubble-bg: #EAF4FF; /* 柔和的浅蓝 */
--user-bubble-bg: #B3E19D; /* 柔和的浅绿 */
--chat-bg: #F0F2F5; /* 整体背景灰 */
--text-color: #303133;
}
.chat-window-container {
display: flex;
flex-direction: column;
height: 100%;
height: 90vh; /* 限制最大高度 */
width: 100%;
max-width: 960px; /* PC端最佳宽度 */
margin: 5vh auto; /* 居中并增加顶部间距 */
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
/* 现代感的轻微阴影 */
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
transition: all 0.3s ease;
}
/* 头部样式优化 */
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: linear-gradient(135deg, #409EFF 0%, #64b5ff 100%);
color: white;
/* 扁平化,用浅色背景和底部阴影代替强渐变 */
background: #FFFFFF;
border-bottom: 1px solid #EBEEF5;
color: var(--text-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
}
.header-left {
@@ -296,117 +363,216 @@ const scrollToBottom = () => {
.header-left h2 {
margin: 0;
font-size: 1.5rem;
font-size: 1.4rem;
font-weight: 600;
color: var(--primary-color);
max-width: 300px; /* 限制标题长度 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 状态指示器美化 */
.status-indicator {
width: 8px;
height: 8px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #67C23A;
display: inline-block;
transition: background-color 0.3s;
}
.status-indicator.ACTIVE {
background-color: #67C23A; /* 绿色 */
}
.status-indicator.COMPLETED {
background: #909399;
background-color: #F56C6C; /* 红色 */
}
.status-text {
font-size: 0.9rem;
opacity: 0.9;
color: #909399;
}
/* 消息区域 */
/* 按钮组样式 */
.header-right {
display: flex;
gap: 10px;
}
.end-button {
border-color: #F56C6C !important;
color: #F56C6C !important;
}
/* 消息区域优化 */
.messages-container {
flex: 1;
overflow-y: auto;
padding: 24px;
background: #f9fafb;
padding: 20px 24px;
background: var(--chat-bg);
/* 滚动条美化Webkit Only*/
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
}
.message-row {
display: flex;
margin-bottom: 24px;
max-width: 80%;
}
.message-ai {
justify-content: flex-start;
margin-bottom: 20px;
max-width: 80%; /* PC端消息宽度 */
align-items: flex-start; /* 消息气泡顶部对齐 */
}
.message-user {
justify-content: flex-end;
margin-left: auto;
flex-direction: row-reverse; /* 用户头像在右边 */
}
.avatar {
flex-shrink: 0;
margin-right: 12px;
font-weight: bold;
/* 通用气泡样式 */
.message-bubble {
padding: 12px 16px;
border-radius: 18px; /* 柔和的圆角 */
font-size: 0.95rem;
line-height: 1.6;
max-width: 100%;
word-break: break-word;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); /* 轻微阴影 */
position: relative;
}
.message-user .avatar {
order: 2;
margin-right: 0;
margin-left: 12px;
/* AI 气泡 */
.message-ai .message-bubble {
background-color: var(--ai-bubble-bg);
color: var(--text-color);
border-bottom-left-radius: 4px; /* 靠近头像的角变小 */
}
/* USER 气泡 */
.message-user .message-bubble {
background-color: var(--user-bubble-bg);
color: #303133;
border-bottom-right-radius: 4px; /* 靠近头像的角变小 */
}
.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;
max-width: calc(100% - 60px); /* 气泡的最大宽度 */
}
.message-time {
font-size: 0.75rem;
color: #909399;
margin-top: 4px;
padding: 0 4px;
align-self: flex-end; /* 时间戳靠右 */
padding: 0 8px;
}
.message-ai .message-time {
text-align: left;
align-self: flex-start; /* AI时间戳靠左 */
}
.message-user .message-time {
text-align: right;
/* AI 思考中动画美化 */
.typing-indicator-row {
margin-bottom: 20px;
}
/* 输入区域 */
.thinking {
display: flex;
align-items: center;
gap: 4px;
background-color: var(--ai-bubble-bg);
min-width: 60px;
height: 40px;
padding: 10px 16px;
border-bottom-left-radius: 4px;
}
@keyframes bounce {
0%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
}
.typing-dot {
width: 6px;
height: 6px;
background-color: var(--primary-color);
border-radius: 50%;
animation: bounce 1.2s infinite;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
/* Markdown 渲染样式穿透 */
:deep(.markdown-body) {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1rem;
line-height: 1.6;
color: inherit; /* 继承气泡颜色,以防万一 */
}
:deep(.markdown-body p) {
margin: 0 0 8px 0;
}
/* 代码块样式 */
:deep(.markdown-body pre) {
background-color: #282c34; /* 深色背景 */
color: #abb2bf;
padding: 1em;
border-radius: 6px;
overflow-x: auto;
max-width: 100%;
font-size: 0.9em;
}
/* 输入区域优化 */
.input-area {
padding: 16px 24px;
border-top: 1px solid #e6e8eb;
background: white;
flex-shrink: 0;
}
.answer-input {
margin-bottom: 12px;
.answer-input :deep(.el-textarea__inner) {
/* 移除 Element Plus 默认的内阴影 */
box-shadow: none !important;
/* 增加边框,使其更清晰 */
border: 1px solid #DCDFE6;
border-radius: 8px;
padding: 10px 15px;
font-size: 0.95rem;
transition: border-color 0.2s;
}
.answer-input :deep(.el-textarea__inner:focus) {
border-color: var(--primary-color);
}
.input-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.char-counter {
@@ -415,77 +581,116 @@ const scrollToBottom = () => {
}
.send-button {
min-width: 100px;
padding: 8px 20px;
font-size: 1rem;
height: auto; /* 自动高度 */
}
/* 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);
background-color: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
align-items: center;
z-index: 10;
backdrop-filter: blur(3px); /* 增加一点模糊效果 */
}
.completion-content {
text-align: center;
max-width: 500px;
padding: 24px;
/* ----------------------------------------------------- */
/* 响应式优化:平板 (769px < width <= 1024px) */
/* ----------------------------------------------------- */
@media (min-width: 769px) and (max-width: 1024px) {
.chat-window-container {
max-width: 95%; /* 平板上使用更大的宽度 */
height: 95vh;
margin: 2.5vh auto;
}
/* 响应式设计 */
.messages-container {
padding: 20px;
}
}
/* ----------------------------------------------------- */
/* 响应式优化:手机 (width <= 768px) */
/* ----------------------------------------------------- */
@media (max-width: 768px) {
.message-row {
max-width: 90%;
.chat-window-container {
height: 100vh; /* 手机上全屏高度 */
max-height: 100vh;
border-radius: 0; /* 移除圆角 */
box-shadow: none; /* 移除阴影 */
max-width: 100%;
margin: 0;
}
.chat-header {
padding: 12px 16px;
flex-wrap: wrap;
align-items: flex-start;
}
.header-left {
flex-basis: 100%; /* 左侧标题占满一行 */
margin-bottom: 8px;
order: 1;
}
.header-left h2 {
font-size: 1.2rem;
max-width: 70%; /* 限制标题长度 */
}
.header-right {
order: 2;
margin-left: auto;
}
.header-button {
font-size: 12px;
padding: 6px 10px;
}
.messages-container {
padding: 16px;
}
/* 手机上消息气泡占 90% */
.message-row {
max-width: 90%;
margin-bottom: 16px;
}
.message-bubble {
font-size: 0.9rem;
padding: 10px 14px;
border-radius: 16px;
}
.input-area {
padding: 12px 16px;
}
.input-actions {
flex-direction: column-reverse; /* 按钮在上,计数器在下 */
align-items: flex-end;
gap: 8px;
}
.char-counter {
align-self: flex-start; /* 计数器左对齐 */
margin-top: 4px;
}
.send-button {
width: 100%; /* 按钮全宽 */
font-size: 0.95rem;
padding: 10px;
}
}
</style>

View File

@@ -1,123 +1,282 @@
<template>
<div class="home-container">
<div class="hero-section">
<h1>AI面试与对话系统</h1>
<p>提升您的面试技能与AI进行智能对话</p>
<div class="hero-content">
<h1 class="animate-in">AI面试与对话系统</h1>
<p class="subtitle animate-in">智能驱动高效提升您的面试技能与专业对话能力</p>
<el-button type="warning" size="large" round class="explore-btn" @click="navigateTo('interview')">
立即开始模拟面试
<el-icon class="el-icon--right"><ArrowRight /></el-icon>
</el-button>
</div>
</div>
<div class="section-title-wrap">
<h2>选择您的学习路径</h2>
<p class="section-subtitle">专业功能助力您在求职道路上脱颖而出</p>
</div>
<div class="options-container">
<!--
<el-card class="option-card" shadow="hover" @click="navigateTo('chat')">
<el-card class="option-card primary-option" shadow="always" @click="navigateTo('interview')">
<div class="card-content">
<el-icon size="48" color="#409EFF">
<ChatLineRound/>
<el-icon size="56" color="#FFFFFF">
<UserFilled/>
</el-icon>
<h3>AI对话</h3>
<p>与AI进行自由对话获取帮助和建议</p>
<el-button type="primary" class="action-btn">开始对话</el-button>
<h3>AI 模拟面试</h3>
<p>针对特定岗位进行实战模拟AI面试官实时反馈快速发现并改进短板</p>
<el-button type="primary" class="action-btn" size="large" round>
开始面试
</el-button>
</div>
</el-card>
-->
<el-card class="option-card" shadow="hover" @click="navigateTo('interview')">
<el-card class="option-card secondary-option" shadow="hover" @click="navigateTo('history')">
<div class="card-content">
<el-icon size="48" color="#67C23A">
<User/>
<el-icon size="56" color="#409EFF">
<Files/>
</el-icon>
<h3>AI面试</h3>
<p>进行模拟面试提升面试技巧</p>
<el-button type="success" class="action-btn">开始面试</el-button>
<h3>回顾与复盘</h3>
<p>查看历史面试记录和AI评分报告系统化复盘每一次对话与问答过程</p>
<el-button type="info" class="action-btn" size="large" plain round>
查看记录
</el-button>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import {useRouter} from 'vue-router'
import {ChatLineRound, User} from '@element-plus/icons-vue'
// 导入优化后的图标
import {UserFilled, Files, ArrowRight} from '@element-plus/icons-vue'
const router = useRouter()
const navigateTo = (type) => {
if (type === 'chat') {
router.push('/chat')
} else if (type === 'interview') {
router.push('/interview')
}
const navigateTo = (path) => {
router.push(`/${path}`)
}
</script>
<style scoped>
/* ----------------------------------------------------- */
/* 基础布局与容器 */
/* ----------------------------------------------------- */
.home-container {
padding: 40px 20px;
max-width: 1000px;
padding: 0 0 60px 0; /* 移除顶部内边距,使 Hero Section 贴顶 */
margin: 0 auto;
text-align: center;
min-height: calc(100vh - 60px); /* 确保页面有足够高度 */
background-color: #f4f6f9;
}
/* ----------------------------------------------------- */
/* 英雄区域 (Hero Section) */
/* ----------------------------------------------------- */
.hero-section {
margin-bottom: 60px;
background: linear-gradient(135deg, #409EFF 0%, #79bbff 100%);
color: white;
padding: 80px 20px;
margin-bottom: 50px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
.hero-content {
max-width: 800px;
margin: 0 auto;
}
.hero-section h1 {
font-size: 2.5rem;
color: #303133;
margin-bottom: 16px;
font-size: 3.5rem;
font-weight: 800;
margin-bottom: 15px;
line-height: 1.2;
}
.hero-section p {
font-size: 1.2rem;
color: #606266;
.subtitle {
font-size: 1.4rem;
font-weight: 300;
margin-bottom: 40px;
opacity: 0.9;
}
.explore-btn {
font-size: 1.1rem;
padding: 25px 35px;
font-weight: 600;
}
/* 动画效果 */
.animate-in {
opacity: 0;
animation: fadeIn 1s forwards;
}
.animate-in:nth-child(1) { animation-delay: 0.2s; }
.animate-in:nth-child(2) { animation-delay: 0.4s; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ----------------------------------------------------- */
/* 核心选项区域 */
/* ----------------------------------------------------- */
.section-title-wrap {
text-align: center;
margin-bottom: 40px;
}
.section-title-wrap h2 {
font-size: 2rem;
color: #303133;
font-weight: 700;
}
.section-subtitle {
font-size: 1.1rem;
color: #909399;
margin-top: 5px;
}
.options-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 30px;
margin-top: 40px;
max-width: 900px;
margin: 0 auto 60px auto;
padding: 0 20px;
}
.option-card {
cursor: pointer;
transition: all 0.3s ease;
height: 250px;
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
border: 1px solid #e4e7ed;
}
.option-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;
transform: translateY(-8px);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.08) !important;
}
.primary-option {
background: #409EFF;
color: white;
}
.primary-option .card-content p {
color: rgba(255, 255, 255, 0.85);
}
.primary-option .action-btn {
background-color: #FFFFFF;
color: #409EFF;
border: none;
font-weight: 600;
}
.primary-option .action-btn:hover {
background-color: #f0f0f0;
}
.secondary-option {
background-color: white;
}
.secondary-option h3 {
color: #303133;
}
.secondary-option p {
color: #606266;
}
.card-content {
text-align: center;
padding: 20px;
padding: 30px;
}
.card-content h3 {
font-size: 1.5rem;
margin: 16px 0;
color: #303133;
font-size: 1.8rem;
margin: 20px 0 10px 0;
font-weight: 600;
}
.card-content p {
color: #606266;
margin-bottom: 24px;
margin-bottom: 30px;
font-size: 1rem;
}
.action-btn {
margin-top: 10px;
font-size: 1rem;
padding: 20px 30px;
transition: all 0.3s ease;
}
/* ----------------------------------------------------- */
/* 响应式优化 (Mobile/Tablet) */
/* ----------------------------------------------------- */
@media (max-width: 1000px) {
.hero-section {
padding: 60px 20px;
}
.hero-section h1 {
font-size: 3rem;
}
}
@media (max-width: 768px) {
.options-container {
grid-template-columns: 1fr;
.hero-section {
padding: 40px 15px;
margin-bottom: 30px;
border-radius: 0; /* 手机上取消圆角 */
}
.hero-section h1 {
font-size: 2rem;
font-size: 2.2rem;
}
.subtitle {
font-size: 1.1rem;
margin-bottom: 30px;
}
.explore-btn {
padding: 18px 25px;
font-size: 1rem;
}
.section-title-wrap h2 {
font-size: 1.5rem;
}
.section-subtitle {
font-size: 0.9rem;
}
.options-container {
grid-template-columns: 1fr;
padding: 0 15px;
}
.option-card {
min-height: 250px;
}
.card-content h3 {
font-size: 1.3rem;
}
.card-content p {
font-size: 0.9rem;
margin-bottom: 20px;
}
.action-btn {
padding: 15px 25px;
}
}
</style>

View File

@@ -2,24 +2,24 @@
<div class="interview-view-container">
<div class="header-section">
<h1>选择面试模式</h1>
<p>根据您的需求选择合适的面试方式</p>
<p>根据您的需求选择合适的面试方式获得最专业的反馈报告</p>
</div>
<div class="mode-cards-container">
<!-- AI智能面试卡片 -->
<el-card
:class="['mode-card', { 'active': selectedMode === 'ai' }]"
shadow="hover"
@click="selectedMode = 'ai'"
>
<div class="card-content">
<div class="card-icon">
<el-icon size="48" color="#409EFF">
<div class="card-icon primary-icon">
<el-icon size="48">
<Cpu/>
</el-icon>
</div>
<h3>AI 智能面试</h3>
<p class="description">AI根据您的简历智能生成个性化面试题目</p>
<p class="description"> **AI** 根据您的简历智能生成个性化面试题目实现**高精度**模拟</p>
<ul class="features-list">
<li>
<el-icon>
@@ -37,26 +37,25 @@
<el-icon>
<Check/>
</el-icon>
实时反馈与评分
**实时**反馈与评分
</li>
</ul>
</div>
</el-card>
<!-- 本地题库面试卡片 -->
<el-card
:class="['mode-card', { 'active': selectedMode === 'local' }]"
shadow="hover"
@click="selectedMode = 'local'"
>
<div class="card-content">
<div class="card-icon">
<el-icon size="48" color="#67C23A">
<div class="card-icon success-icon">
<el-icon size="48">
<Collection/>
</el-icon>
</div>
<h3>本地题库面试</h3>
<p class="description">从预设题库中选择题目进行系统化面试</p>
<p class="description">从预设题库中选择题目进行系统化面试**稳固**基础知识</p>
<ul class="features-list">
<li>
<el-icon>
@@ -68,7 +67,7 @@
<el-icon>
<Check/>
</el-icon>
可自定义选择范围
**自定义**选择范围
</li>
<li>
<el-icon>
@@ -81,16 +80,41 @@
</el-card>
</div>
<!-- 题库选择区域仅本地模式显示 -->
<QuestionBankSection ref="questionBankSectionRef" v-if="selectedMode === 'local'"/>
<div class="question-bank-section" v-if="selectedMode === 'local'">
<el-alert title="请从下方题库中选择您希望参与面试的知识点或题集范围" type="info" :closable="false" center />
<QuestionBankSection ref="questionBankSectionRef"/>
</div>
<!-- 开始面试表单 -->
<div class="start-form-section">
<el-card shadow="never">
<el-form :model="formData" label-width="120px" size="large">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>{{ selectedMode === 'ai' ? 'AI 面试启动配置' : '题库面试启动配置' }}</span>
</div>
</template>
<el-form :model="formData" size="large" label-position="top"> <el-row :gutter="20">
<el-col :span="isMobile ? 24 : 12">
<el-form-item label="您的姓名" required>
<el-input v-model="formData.candidateName" placeholder="请输入您的姓名"/>
</el-form-item>
</el-col>
<el-col :span="isMobile ? 24 : 12">
<el-form-item label="面试题目数量" prop="totalQuestions">
<el-input-number v-model="formData.totalQuestions" :min="1" :max="100" controls-position="right"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="目标岗位要求" required>
<el-input
v-model="formData.jobRequirements"
type="textarea"
:rows="4"
placeholder="请输入目标岗位的名称和核心要求例如3年经验的前端开发熟悉Vue3、TypeScript。AI 将根据此信息出题。"
/>
</el-form-item>
<el-form-item label="上传简历" required>
<el-upload
@@ -100,37 +124,39 @@
:auto-upload="false"
:on-change="handleFileChange"
:on-exceed="handleFileExceed"
:file-list="fileList"
>
<template #trigger>
<el-button type="primary">选择文件</el-button>
</template>
<template #tip>
<div class="upload-tip">
支持 PDFMarkdown 或文本格式大小不超过10MB
支持 PDFMarkdown 或文本格式大小不超过10MB简历是 AI 个性化出题的基础
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="AI 模型">
<el-select v-model="formData.aiModel" placeholder="请选择AI模型">
<el-option label="DeepSeek" value="deepSeek"></el-option>
<el-option label="阿里千问" value="qwen"></el-option>
</el-select>
</el-form-item>
<el-form-item label="面试题目数量">
<el-input-number v-model="formData.totalQuestions" :min="1" :max="100"></el-input-number>
</el-form-item>
<el-form-item>
<el-divider />
<el-form-item class="form-actions">
<el-button
type="success"
:loading="isLoading"
@click="startInterviewAction"
class="start-button"
size="large"
>
{{ selectedMode === 'ai' ? '开始 AI 面试' : '开始题库面试' }}
</el-button>
<el-button @click="$router.push('/')">返回首页</el-button>
<el-button @click="$router.push('/')" size="large">返回首页</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -139,10 +165,10 @@
</template>
<script setup>
import {ref, reactive} from 'vue'
import {ref, reactive, computed, onMounted, onBeforeUnmount} from 'vue'
import {useRouter} from 'vue-router'
import {ElMessage} from 'element-plus'
import {Cpu, Collection, Check} from '@element-plus/icons-vue'
import {ElMessage, ElNotification} from 'element-plus'
import {Cpu, Collection, Check, Finished} from '@element-plus/icons-vue' // 引入 Finished 图标用于上传成功提示
import QuestionBankSection from '@/components/QuestionBankSection.vue'
import {startInterview} from '@/api/interview.js';
@@ -151,52 +177,118 @@ const router = useRouter()
const questionBankSectionRef = ref(null)
// 响应式状态
// --- 响应式状态 ---
const selectedMode = ref('ai') // 'ai' 或 'local'
const isLoading = ref(false)
const fileList = ref([]) // 用于显示已上传的文件列表
// 表单数据
const formData = ref({
candidateName: '',
resumeFiles: [],
jobRequirements: '',
resumeFiles: null,
totalQuestions: 10,
aiModel: 'deepSeek'
})
// --- 响应式断点控制 ---
const windowWidth = ref(window.innerWidth);
const MOBILE_WIDTH = 768;
// --- UI交互方法 ---
const handleFileChange = (file) => {
formData.value.resumeFiles = file.raw;
const isMobile = computed(() => windowWidth.value <= MOBILE_WIDTH);
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
// 文件上传处理
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
// --- 文件上传处理 ---
const handleFileChange = (file) => {
// 清除旧文件
fileList.value = []
// 限制文件类型和大小(虽然主要由后端校验,前端也应提醒)
const isAcceptedType = ['application/pdf', 'text/markdown', 'text/plain'].includes(file.raw.type) || file.name.endsWith('.md');
const isLt10M = file.raw.size / 1024 / 1024 < 10;
if (!isAcceptedType) {
ElMessage.error('只支持 PDF, Markdown (.md) 或文本 (.txt) 文件!');
return false;
}
if (!isLt10M) {
ElMessage.error('文件大小不能超过 10MB!');
return false;
}
formData.value.resumeFiles = file.raw;
fileList.value.push({name: file.name, url: URL.createObjectURL(file.raw), status: 'success', uid: file.uid})
ElNotification({
title: '简历上传成功',
message: `${file.name} 已准备就绪。`,
type: 'success',
icon: Finished,
duration: 3000
});
};
const handleFileExceed = () => {
ElMessage.warning('只能上传一个简历文件')
}
// --- 业务逻辑 ---
const sessionId = ref('')
// 开始面试
const validateForm = () => {
if (!formData.value.candidateName) {
ElMessage.error('请输入您的姓名。');
return false;
}
if (!formData.value.jobRequirements) {
ElMessage.error('请输入目标岗位要求。');
return false;
}
if (!formData.value.resumeFiles) {
ElMessage.error('请上传您的简历文件。');
return false;
}
if (selectedMode.value === 'local' && (!questionBankSectionRef.value || questionBankSectionRef.value.getSelectionResult().selectedNodes.length === 0)) {
ElMessage.error('请在本地题库模式下选择至少一个知识点或题集。');
return false;
}
return true;
}
const startInterviewAction = async () => {
console.log(formData.value)
if (!formData.value.candidateName || !formData.value.resumeFiles) {
ElMessage.error('请输入您的姓名并上传简历。');
if (!validateForm()) {
return;
}
isLoading.value = true;
const sendFormData = new FormData();
// 准备发送到后端的 JSON 数据
const sendData = {
candidateName: formData.value.candidateName,
jobRequirements: formData.value.jobRequirements,
aiModel: formData.value.aiModel,
totalQuestions: formData.value.totalQuestions,
model: selectedMode.value,
selectedNodes: []
}
// 处理本地题库模式的选中节点
if (selectedMode.value === 'local') {
const selectionResult = questionBankSectionRef.value.getSelectionResult()
if (!selectionResult.selectedNodes) {
selectionResult.selectedNodes = []
}
if (selectionResult.selectedNodes && selectionResult.selectedNodes.length > 0) {
const selectionResult = questionBankSectionRef.value?.getSelectionResult()
if (selectionResult && selectionResult.selectedNodes && selectionResult.selectedNodes.length > 0) {
const sendNodes = []
selectionResult.selectedNodes.forEach(node => {
sendNodes.push({
@@ -209,15 +301,18 @@ const startInterviewAction = async () => {
}
}
// 将 JSON 数据作为 Blob 添加到 FormData
sendFormData.append('interviewStartDto', new Blob([JSON.stringify(sendData)], {
type: 'application/json',
}))
// 添加简历文件
sendFormData.append('resume', formData.value.resumeFiles);
try {
console.log(sendFormData.values())
const responseData = await startInterview(sendFormData);
const data = responseData.data;
sessionId.value = data.sessionId;
// 跳转到聊天界面
router.push({
path: '/interview-chat',
@@ -228,101 +323,132 @@ const startInterviewAction = async () => {
})
} catch (error) {
console.error('开始面试失败:', error);
ElMessage.error(`开始面试失败: ${error.message || '网络错误'}`);
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
/* 1. PC/宽屏 (默认样式) */
.interview-view-container {
padding: 24px;
max-width: 1200px;
padding: 30px 24px;
max-width: 1000px;
margin: 0 auto;
}
.header-section {
text-align: center;
margin-bottom: 32px;
margin-bottom: 40px;
}
.header-section h1 {
font-size: 2.5rem;
color: #303133;
margin-bottom: 8px;
margin-bottom: 10px;
font-weight: 700;
}
.header-section p {
font-size: 1.1rem;
color: #606266;
color: #909399;
}
/* --- 模式选择卡片 --- */
.mode-cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 24px;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 30px;
margin-bottom: 30px;
}
.mode-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
border: 2px solid #e4e7ed;
border-radius: 12px;
}
.mode-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
transform: translateY(-6px);
box-shadow: 0 12px 20px rgba(0, 0, 0, 0.1);
}
.mode-card.active {
border-color: #409EFF;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
}
.card-content {
text-align: center;
padding: 20px;
background-color: #f7f9fc;
box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
}
.card-content { text-align: center; padding: 20px; }
.card-icon {
margin-bottom: 16px;
margin-bottom: 20px;
/* 使得图标更突出 */
display: inline-flex;
padding: 15px;
border-radius: 50%;
}
.primary-icon { background-color: #ecf5ff; color: #409EFF; }
.success-icon { background-color: #f0f9eb; color: #67C23A; }
.card-content h3 {
font-size: 1.5rem;
margin-bottom: 12px;
color: #303133;
}
.description {
color: #606266;
margin-bottom: 16px;
line-height: 1.6;
}
.card-content h3 { font-size: 1.6rem; margin-bottom: 10px; color: #303133; font-weight: 600; }
.description { color: #909399; margin-bottom: 20px; line-height: 1.6; font-size: 0.95rem; }
.features-list {
list-style: none;
padding: 0;
margin: 0;
text-align: left;
max-width: 250px;
margin: 0 auto;
}
.features-list li {
padding: 8px 0;
padding: 6px 0;
color: #606266;
display: flex;
align-items: center;
font-size: 0.9rem;
}
.features-list .el-icon {
color: #67C23A;
margin-right: 8px;
margin-right: 10px;
font-size: 16px;
}
/* --- 题库选择区 --- */
.question-bank-section {
margin-bottom: 30px;
padding: 15px;
border: 1px dashed #dcdfe6;
border-radius: 8px;
background-color: #fcfcfc;
}
/* --- 启动表单区域 --- */
.start-form-section {
margin-top: 24px;
margin-top: 30px;
}
.start-form-section .el-card {
border-radius: 12px;
}
.card-header span {
font-size: 1.2rem;
font-weight: 600;
color: #303133;
}
/* 覆盖 Element Plus 默认样式,使用 label-position="top" */
.el-form :deep(.el-form-item) {
margin-bottom: 25px;
}
.el-form :deep(.el-form-item__label) {
font-size: 1rem;
color: #303133;
line-height: 2;
}
.upload-tip {
@@ -331,17 +457,76 @@ const startInterviewAction = async () => {
margin-top: 8px;
}
.start-button {
min-width: 140px;
.el-input-number {
width: 100%;
}
.el-select {
width: 100%;
}
.form-actions {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
.start-button {
min-width: 160px;
}
/* 2. 平板/小型PC优化 (769px < width <= 1000px) */
@media (min-width: 769px) and (max-width: 1000px) {
.interview-view-container {
padding: 24px;
}
}
/* 3. 手机/超小平板优化 (width <= 768px) */
@media (max-width: 768px) {
.mode-cards-container {
grid-template-columns: 1fr;
.interview-view-container {
padding: 16px;
}
.header-section h1 {
font-size: 2rem;
}
.header-section p {
font-size: 0.9rem;
}
/* 卡片堆叠成单列 */
.mode-cards-container {
grid-template-columns: 1fr;
gap: 16px;
}
/* 卡片内容简化 */
.card-content h3 { font-size: 1.4rem; }
.description { font-size: 0.9rem; }
.features-list { max-width: none; } /* 移动端列表全宽 */
/* 启动表单:强制全宽 */
.el-row :deep(.el-col) {
width: 100%;
margin-bottom: 0;
}
/* 优化按钮区域布局 */
.form-actions :deep(.el-form-item__content) {
display: flex;
flex-direction: column;
gap: 10px;
.el-button {
width: 100%;
}
}
/* 移除数字输入框右侧边距 */
.el-form-item .el-input-number {
width: 100% !important;
}
}
</style>

View File

@@ -13,14 +13,14 @@ export default defineConfig({
},
server: {
host: '0.0.0.0',
// proxy: {
// // Proxy API requests to the backend server
proxy: {
// Proxy API requests to the backend server
// '/api': {
// target: 'http://localhost:8080',
// changeOrigin: true, // Needed for virtual hosted sites
// secure: false, // Optional: if you are using https
// rewrite: (path) => path.replace(/^\/api/, ''),
// },
// },
},
},
})