初始化
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
# AI-interview-web
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1838
package-lock.json
generated
Normal file
1838
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "ai-interview-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
8
src/api/dashboard.js
Normal file
8
src/api/dashboard.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import apiClient from './index';
|
||||
|
||||
/**
|
||||
* 获取仪表盘的所有统计数据
|
||||
*/
|
||||
export const getDashboardStats = () => {
|
||||
return apiClient.post('/dashboard/stats');
|
||||
};
|
||||
33
src/api/index.js
Normal file
33
src/api/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// Create an Axios instance with a base configuration
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 600000, // 10 min timeout
|
||||
});
|
||||
|
||||
// Optional: Add a response interceptor for global error handling
|
||||
apiClient.interceptors.response.use(
|
||||
response => {
|
||||
// Check if the response has the expected successful structure
|
||||
if (response.data && response.data.code === 0) {
|
||||
return response.data; // Return only the data part of the response
|
||||
} else {
|
||||
// Handle business errors (e.g., code !== 200)
|
||||
const errorMessage = response.data.message || 'An unknown error occurred.';
|
||||
ElMessage.error(errorMessage);
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
// Handle HTTP errors (e.g., 4xx, 5xx)
|
||||
const errorMessage = error.response?.data?.message || 'A network error occurred. Please check your connection.';
|
||||
ElMessage.error(errorMessage);
|
||||
console.error('API Error:', error.response || error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
|
||||
37
src/api/interview.js
Normal file
37
src/api/interview.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import apiClient from './index';
|
||||
|
||||
/**
|
||||
* 开始新的面试
|
||||
* @param {FormData} formData - 包含简历和候选人信息的表单数据
|
||||
*/
|
||||
export const startInterview = (formData) => {
|
||||
console.log(formData)
|
||||
return apiClient.post('/interview/start', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 继续面试(发送回答)
|
||||
* @param {object} data - 包含sessionId和userAnswer的数据
|
||||
*/
|
||||
export const continueInterview = (data) => {
|
||||
return apiClient.post('/interview/chat', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取面试历史列表
|
||||
*/
|
||||
export const getInterviewHistoryList = () => {
|
||||
return apiClient.post('/interview/get-history-list');
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取详细的面试复盘报告
|
||||
* @param {string} sessionId - 面试会话ID
|
||||
*/
|
||||
export const getInterviewReportDetail = (sessionId) => {
|
||||
return apiClient.post('/interview/get-report-detail', { sessionId });
|
||||
};
|
||||
5
src/api/question-progress.js
Normal file
5
src/api/question-progress.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import apiClient from './index';
|
||||
|
||||
export const pageList = (params) => {
|
||||
return apiClient.post('/interview-question-progress/page', params);
|
||||
}
|
||||
53
src/api/question.js
Normal file
53
src/api/question.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import apiClient from './index';
|
||||
|
||||
/**
|
||||
* 分页获取题库列表
|
||||
* @param {object} params - 分页和查询参数
|
||||
*/
|
||||
export const getQuestionPage = (params) => {
|
||||
return apiClient.post('/question/page', params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增题目
|
||||
* @param {object} data - 题目数据
|
||||
*/
|
||||
export const addQuestion = (data) => {
|
||||
return apiClient.post('/question/add', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新题目
|
||||
* @param {object} data - 题目数据
|
||||
*/
|
||||
export const updateQuestion = (data) => {
|
||||
return apiClient.post('/question/update', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除题目
|
||||
* @param {number} id - 题目ID
|
||||
*/
|
||||
export const deleteQuestion = (id) => {
|
||||
return apiClient.post('/question/delete', { id });
|
||||
};
|
||||
|
||||
/**
|
||||
* AI批量导入题目
|
||||
* @param {FormData} formData - 包含文件的表单数据
|
||||
*/
|
||||
export const importQuestionsByAi = (formData) => {
|
||||
return apiClient.post('/question/import-by-ai', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验重复数据
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export const checkQuestionData = () => {
|
||||
return apiClient.post('/question/check-question-data');
|
||||
}
|
||||
135
src/ard.vue
Normal file
135
src/ard.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<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>
|
||||
23
src/assets/css/reset.css
Normal file
23
src/assets/css/reset.css
Normal file
@@ -0,0 +1,23 @@
|
||||
body, html, h1, h2, h3, h4, h5, h6, ul, ol, li, dl, dt, dd, header, menu, section, p, input, td, th, ins {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
ul, li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
button {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
1
src/assets/dashboard-hero.svg
Normal file
1
src/assets/dashboard-hero.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M112 0C85.5 0 64 21.5 64 48v48H48c-26.5 0-48 21.5-48 48v80c0 26.5 21.5 48 48 48h16v32c0 26.5 21.5 48 48 48h320c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H112zM48 144c-8.8 0-16 7.2-16 16v80c0 8.8 7.2 16 16 16h16v96H112c-8.8 0-16-7.2-16-16V48c0-8.8 7.2-16 16-16h352c8.8 0 16 7.2 16 16v288c0 8.8-7.2 16-16 16H112v-32h48c26.5 0 48-21.5 48-48V144H48z" fill="#a0aec0"/><path d="M176 224c-17.7 0-32 14.3-32 32s14.3 32 32 32h160c17.7 0 32-14.3 32-32s-14.3-32-32-32H176z" fill="#718096"/></svg>
|
||||
|
After Width: | Height: | Size: 560 B |
1
src/assets/interview-start.svg
Normal file
1
src/assets/interview-start.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M176 96c0-35.3 28.7-64 64-64s64 28.7 64 64v32H176V96z" fill="#a0aec0"/><path d="M240 32c-44.2 0-80 35.8-80 80v32h160V112c0-44.2-35.8-80-80-80z" fill="#718096"/><path d="M0 192c0-35.3 28.7-64 64-64h384c35.3 0 64 28.7 64 64v256c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192zm64 32c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h384c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32H64z" fill="#a0aec0"/><path d="M160 320c-17.7 0-32 14.3-32 32s14.3 32 32 32h192c17.7 0 32-14.3 32-32s-14.3-32-32-32H160z" fill="#718096"/></svg>
|
||||
|
After Width: | Height: | Size: 596 B |
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
137
src/layout/Layout.vue
Normal file
137
src/layout/Layout.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<!-- 整体后台布局容器 -->
|
||||
<el-container class="layout-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="220px" class="sidebar">
|
||||
<!-- Logo区域 -->
|
||||
<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"
|
||||
background-color="#0a192f"
|
||||
text-color="#8892b0"
|
||||
active-text-color="#64ffda"
|
||||
:router="true"
|
||||
>
|
||||
<el-menu-item index="/">
|
||||
<el-icon><House /></el-icon>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/interview">
|
||||
<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="/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-aside>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-title">欢迎使用AI模拟面试平台</div>
|
||||
</el-header>
|
||||
<el-main class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {House, ChatLineRound, MessageBox, ChatDotSquare, Finished, List} from '@element-plus/icons-vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 更新后的侧边栏样式 */
|
||||
.sidebar {
|
||||
background-color: #0a192f; /* 更深、更现代的科技蓝 */
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
background-color: #0a192f;
|
||||
color: #ccd6f6;
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
margin-right: 12px;
|
||||
color: #64ffda; /* 高亮颜色 */
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 菜单项激活时的样式 */
|
||||
.el-menu-item.is-active {
|
||||
background-color: #112240 !important; /* 深色背景 */
|
||||
border-left: 3px solid #64ffda; /* 左侧高亮条 */
|
||||
}
|
||||
|
||||
.el-menu-item:hover {
|
||||
background-color: #112240; /* 悬浮背景色 */
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.1em;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.fade-transform-enter-active,
|
||||
.fade-transform-leave-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from,
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
</style>
|
||||
13
src/main.js
Normal file
13
src/main.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import router from "./router/index.js";
|
||||
import 'element-plus/dist/index.css'
|
||||
import './assets/css/reset.css'
|
||||
import "normalize.css";
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.use( router)
|
||||
app.mount('#app')
|
||||
135
src/rd.vue
Normal file
135
src/rd.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<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>
|
||||
50
src/router/index.js
Normal file
50
src/router/index.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import Layout from '../layout/Layout.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
},
|
||||
{
|
||||
path: 'interview',
|
||||
name: 'Interview',
|
||||
component: () => import('../views/InterviewView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'question-bank',
|
||||
name: 'QuestionBank',
|
||||
component: () => import('../views/QuestionBank.vue'),
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
name: 'InterviewHistory',
|
||||
component: () => import('../views/InterviewHistory.vue'),
|
||||
},
|
||||
{
|
||||
path: 'report/:sessionId',
|
||||
name: 'InterviewReport',
|
||||
component: () => import('../views/InterviewReport.vue'),
|
||||
props: true, // 将路由参数作为props传递给组件
|
||||
},
|
||||
{
|
||||
path: 'answer-record',
|
||||
name: 'AnswerRecord',
|
||||
component: () => import('../views/AnswerRecord.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
103
src/views/AnswerRecord.vue
Normal file
103
src/views/AnswerRecord.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import type { ComponentSize } from 'element-plus'
|
||||
import { pageList } from '../api/question-progress'
|
||||
|
||||
const tableData = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const size = ref<ComponentSize>('default')
|
||||
const background = ref(false)
|
||||
const disabled = ref(false)
|
||||
const handleSizeChange = (val: number) => {
|
||||
console.log(`${val} items per page`)
|
||||
pageSize.value = val
|
||||
fetchData()
|
||||
}
|
||||
const total = ref(0)
|
||||
const handleCurrentChange = (val: number) => {
|
||||
console.log(`current page: ${val}`)
|
||||
currentPage.value = val
|
||||
fetchData()
|
||||
|
||||
}
|
||||
const searchContent = ref('')
|
||||
const fetchData = () => {
|
||||
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
|
||||
|
||||
})
|
||||
}
|
||||
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>
|
||||
<!-- 数据部分-->
|
||||
|
||||
<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-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>
|
||||
|
||||
<style scoped lang="css">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
150
src/views/Dashboard.vue
Normal file
150
src/views/Dashboard.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<el-row :gutter="20" class="stats-cards">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic :value="stats.totalInterviews">
|
||||
<template #title>
|
||||
<div class="statistic-title">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>面试总次数</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic :value="stats.totalQuestions">
|
||||
<template #title>
|
||||
<div class="statistic-title">
|
||||
<el-icon><MessageBox /></el-icon>
|
||||
<span>题库总题数</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- ECharts图表 -->
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">题库分类占比</div>
|
||||
</template>
|
||||
<div ref="categoryChart" style="height: 400px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">最近7日面试次数</div>
|
||||
</template>
|
||||
<div ref="dailyChart" style="height: 400px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入Vue核心功能、ECharts、API客户端和图标
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { getDashboardStats } from '../api/dashboard';
|
||||
import { DataAnalysis, MessageBox } from '@element-plus/icons-vue';
|
||||
|
||||
// --- 响应式状态定义 ---
|
||||
const stats = ref({
|
||||
totalInterviews: 0,
|
||||
totalQuestions: 0,
|
||||
questionCategoryStats: [],
|
||||
recentInterviewStats: [],
|
||||
});
|
||||
|
||||
// ECharts实例的DOM引用
|
||||
const categoryChart = ref(null);
|
||||
const dailyChart = ref(null);
|
||||
|
||||
// --- ECharts图表配置与渲染 ---
|
||||
|
||||
// 渲染题库分类饼图
|
||||
const renderCategoryChart = () => {
|
||||
const chartInstance = echarts.init(categoryChart.value);
|
||||
const option = {
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { top: '5%', left: 'center' },
|
||||
series: [
|
||||
{
|
||||
name: '题目分类',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: { label: { show: true, fontSize: '20', fontWeight: 'bold' } },
|
||||
labelLine: { show: false },
|
||||
data: stats.value.questionCategoryStats,
|
||||
},
|
||||
],
|
||||
};
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 渲染每日面试次数柱状图
|
||||
const renderDailyChart = () => {
|
||||
const chartInstance = echarts.init(dailyChart.value);
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: stats.value.recentInterviewStats.map(item => item.date),
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '面试次数',
|
||||
type: 'bar',
|
||||
data: stats.value.recentInterviewStats.map(item => item.count),
|
||||
barWidth: '60%',
|
||||
},
|
||||
],
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
};
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// --- API交互方法 ---
|
||||
|
||||
// 获取所有仪表盘数据
|
||||
const fetchDashboardStats = async () => {
|
||||
try {
|
||||
const responseData = await getDashboardStats();
|
||||
stats.value = responseData.data;
|
||||
// 数据获取后,在下一个DOM更新周期渲染图表
|
||||
nextTick(() => {
|
||||
renderCategoryChart();
|
||||
renderDailyChart();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取仪表盘数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
fetchDashboardStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container { padding: 10px; }
|
||||
.stats-cards { margin-bottom: 20px; }
|
||||
.statistic-title { display: flex; align-items: center; color: #606266; font-size: 14px; }
|
||||
.statistic-title .el-icon { margin-right: 8px; }
|
||||
.chart-row { margin-top: 20px; }
|
||||
.chart-card { border: none; }
|
||||
.card-header { font-size: 1.1em; font-weight: bold; }
|
||||
</style>
|
||||
151
src/views/InterviewHistory.vue
Normal file
151
src/views/InterviewHistory.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="history-container">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<!-- 卡片头部 -->
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>会话列表</span>
|
||||
<el-tooltip class="box-item" effect="dark" content="刷新列表" placement="top">
|
||||
<el-button :icon="Refresh" circle @click="fetchSessions" :loading="isLoading"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 加载中的骨架屏 -->
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<el-skeleton :rows="5" animated />
|
||||
</div>
|
||||
|
||||
<!-- 无数据时的空状态 -->
|
||||
<div v-else-if="sessions.length === 0" class="empty-container">
|
||||
<el-empty description="暂无面试记录,快去开始一场模拟面试吧!" />
|
||||
</div>
|
||||
|
||||
<!-- 历史记录列表 -->
|
||||
<div v-else class="history-list">
|
||||
<el-card v-for="session in sessions" :key="session.id" class="session-card" shadow="hover">
|
||||
<div class="session-content">
|
||||
<div class="session-info">
|
||||
<h4>{{ session.candidateName }} 的面试</h4>
|
||||
<div class="info-row">
|
||||
<el-icon>
|
||||
<Calendar />
|
||||
</el-icon>
|
||||
<span>面试日期: {{ new Date(session.createdTime).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<el-icon>
|
||||
<MessageBox />
|
||||
</el-icon>
|
||||
<span>面试状态:
|
||||
<el-tag :type="session.status === 'COMPLETED' ? 'success' : 'info'" size="small">{{ session.status
|
||||
}}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
<el-button v-if="session.status === 'COMPLETED'" type="primary" plain
|
||||
@click="viewReport(session.sessionId)">查看复盘报告</el-button>
|
||||
<el-button v-else type="primary"
|
||||
@click="$router.push({ path: '/interview', query: { sessionId: session.sessionId } })">继续答题</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入Vue核心功能、路由和API客户端
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getInterviewHistoryList } from '../api/interview';
|
||||
import { Calendar, MessageBox, Refresh } from '@element-plus/icons-vue';
|
||||
|
||||
// --- 响应式状态定义 ---
|
||||
const sessions = ref([]); // 存储面试会话列表
|
||||
const isLoading = ref(false); // 加载状态
|
||||
const router = useRouter(); // Vue Router实例
|
||||
|
||||
// --- API交互方法 ---
|
||||
|
||||
// 获取面试历史列表
|
||||
const fetchSessions = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const responseData = await getInterviewHistoryList();
|
||||
// 按创建时间倒序排序,最新的在最前面
|
||||
sessions.value = responseData.data.sort((a, b) => new Date(b.createdTime) - new Date(a.createdTime));
|
||||
} catch (error) {
|
||||
console.error('获取面试历史失败:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 事件处理 ---
|
||||
|
||||
// 跳转到详细的复盘报告页面
|
||||
const viewReport = (sessionId) => {
|
||||
router.push({ name: 'InterviewReport', params: { sessionId } });
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
|
||||
onMounted(() => {
|
||||
fetchSessions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-container {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
margin-bottom: 20px;
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.session-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.session-info h4 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
color: #606266;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.info-row .el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
</style>
|
||||
176
src/views/InterviewReport.vue
Normal file
176
src/views/InterviewReport.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="report-container">
|
||||
<!-- 加载中的骨架屏 -->
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<el-skeleton :rows="10" animated />
|
||||
</div>
|
||||
|
||||
<!-- 无数据时的空状态 -->
|
||||
<div v-else-if="!reportData" class="empty-container">
|
||||
<el-empty description="无法加载面试报告,请返回重试。" />
|
||||
</div>
|
||||
|
||||
<!-- 报告主内容 -->
|
||||
<div v-else>
|
||||
<!-- 报告头部 -->
|
||||
<el-page-header @back="goBack" class="report-header">
|
||||
<template #content>
|
||||
<div class="header-content">
|
||||
<span class="title">{{ reportData.sessionDetails.candidateName }} 的面试复盘报告</span>
|
||||
<el-tag size="large">{{ new Date(reportData.sessionDetails.createdTime).toLocaleString() }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<!-- AI最终评估报告 -->
|
||||
<el-card class="box-card report-summary" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>AI 最终评估报告</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="finalReport" class="summary-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="综合得分" :value="finalReport.overallScore" />
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-statistic title="录用建议">
|
||||
<template #formatter>
|
||||
<el-tag :type="getRecommendationType(finalReport.hiringRecommendation)" size="large" effect="dark">{{ finalReport.hiringRecommendation }}</el-tag>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider />
|
||||
<h4>综合评语</h4>
|
||||
<p class="feedback-paragraph">{{ finalReport.overallFeedback }}</p>
|
||||
<h4>技术能力评估</h4>
|
||||
<ul class="assessment-list">
|
||||
<li v-for="(value, key) in finalReport.technicalAssessment" :key="key"><strong>{{ key }}:</strong> {{ value }}</li>
|
||||
</ul>
|
||||
<h4>改进建议</h4>
|
||||
<ol class="suggestions-list">
|
||||
<li v-for="suggestion in finalReport.suggestions" :key="suggestion">{{ suggestion }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div v-else><el-empty description="AI最终报告正在生成中或生成失败。" /></div>
|
||||
</el-card>
|
||||
|
||||
<!-- 问答详情 -->
|
||||
<el-card class="box-card question-details" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span>问答详情与逐题评估</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-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="hover">
|
||||
<h4>{{ item.questionContent }}</h4>
|
||||
<p class="user-answer"><strong>您的回答:</strong> {{ item.userAnswer }}</p>
|
||||
<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" score-template="{value} 分" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入Vue核心功能、路由、API客户端和图标
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getInterviewReportDetail } from '../api/interview';
|
||||
import { DataAnalysis, ChatDotRound } from '@element-plus/icons-vue';
|
||||
|
||||
// --- Props & Router ---
|
||||
const props = defineProps({ sessionId: { type: String, required: true } });
|
||||
const router = useRouter();
|
||||
|
||||
// --- 响应式状态定义 ---
|
||||
const reportData = ref(null); // 存储完整的报告数据
|
||||
const isLoading = ref(false); // 加载状态
|
||||
|
||||
// --- 计算属性 ---
|
||||
|
||||
// 安全地解析最终报告的JSON字符串
|
||||
const finalReport = computed(() => {
|
||||
if (reportData.value && reportData.value.sessionDetails.finalReport) {
|
||||
try {
|
||||
return JSON.parse(reportData.value.sessionDetails.finalReport);
|
||||
} catch (e) {
|
||||
console.error('解析最终报告JSON失败:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// --- API交互方法 ---
|
||||
|
||||
// 获取面试报告详情
|
||||
const fetchReport = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const responseData = await getInterviewReportDetail(props.sessionId);
|
||||
reportData.value = responseData.data;
|
||||
} catch (error) {
|
||||
console.error('获取面试报告失败:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 事件处理 ---
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => router.push('/history');
|
||||
|
||||
// 根据录用建议返回不同的标签类型
|
||||
const getRecommendationType = (rec) => {
|
||||
if (rec === '强烈推荐' || rec === '推荐') return 'success';
|
||||
if (rec === '待考虑') return 'warning';
|
||||
if (rec === '不推荐') return 'danger';
|
||||
return 'info';
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
|
||||
onMounted(() => {
|
||||
fetchReport();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.report-container { padding: 10px; }
|
||||
.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); }
|
||||
.header-content { display: flex; align-items: center; justify-content: space-between; width: 100%; }
|
||||
.header-content .title { font-size: 1.2em; font-weight: 600; }
|
||||
|
||||
.box-card { margin-bottom: 20px; border: none; }
|
||||
.card-header { font-size: 1.1em; font-weight: bold; display: flex; align-items: center; }
|
||||
.card-header .el-icon { margin-right: 10px; }
|
||||
|
||||
.summary-content { padding: 10px; }
|
||||
.report-summary h4 { margin: 25px 0 10px 0; font-size: 1.05em; }
|
||||
.report-summary p, .report-summary li { color: #606266; line-height: 1.8; }
|
||||
.feedback-paragraph { text-indent: 2em; }
|
||||
.assessment-list, .suggestions-list { padding-left: 20px; }
|
||||
|
||||
.question-card { margin-top: 10px; }
|
||||
.user-answer { color: #303133; font-style: italic; }
|
||||
.feedback-section { background-color: #f9fafb; padding: 15px; border-radius: 4px; margin-top: 15px; }
|
||||
.score-section { display: flex; align-items: center; margin-top: 10px; }
|
||||
.el-rate { margin-left: 10px; }
|
||||
</style>
|
||||
408
src/views/InterviewView.vue
Normal file
408
src/views/InterviewView.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<div class="interview-view-container">
|
||||
<!-- 面试未开始时的启动界面 -->
|
||||
<div v-if="!interviewStarted" class="start-screen">
|
||||
<el-card class="start-card" shadow="never">
|
||||
<div class="start-content">
|
||||
<img src="/src/assets/interview-start.svg" alt="开始面试插图" class="start-illustration"/>
|
||||
<div class="start-form">
|
||||
<h2>准备开始您的模拟面试</h2>
|
||||
<p>请填写您的信息并上传简历,AI面试官已准备就绪。</p>
|
||||
<el-form label-position="top" @submit.prevent="startInterview">
|
||||
<el-form-item label="您的姓名" required>
|
||||
<el-input v-model="candidateName" placeholder="请输入您的姓名" size="large"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传您的简历" required>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:limit="1"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-exceed="handleFileExceed"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button type="primary" size="large">选择简历文件</el-button>
|
||||
</template>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持PDF或Markdown格式,文件大小不超过10MB。
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="success" @click="startInterviewAction" :loading="isLoading" size="large"
|
||||
class="start-button">开始面试
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 面试开始后的聊天窗口 -->
|
||||
<div v-else class="chat-window-container">
|
||||
<div class="chat-window">
|
||||
<!-- 消息展示区 -->
|
||||
<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>
|
||||
</div>
|
||||
<!-- AI思考中的动画 -->
|
||||
<div v-if="isAiThinking" class="message-row message-ai">
|
||||
<el-avatar class="avatar" :style="{ backgroundColor: '#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"
|
||||
></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-message">
|
||||
<el-alert title="面试已结束" type="success" show-icon :closable="false">
|
||||
感谢您的参与!您可以从左侧菜单开始新的面试或管理题库。
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入Vue核心功能、API客户端和Element Plus组件
|
||||
import {ref, nextTick, onMounted} from 'vue';
|
||||
import {startInterview, continueInterview, getInterviewReportDetail} from '../api/interview';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
// --- 响应式状态定义 ---
|
||||
const interviewStarted = ref(false); // 面试是否已开始
|
||||
const isLoading = ref(false); // 全局加载状态,用于按钮和输入框
|
||||
const isAiThinking = ref(false); // AI是否正在生成回答
|
||||
const candidateName = ref(''); // 候选人姓名
|
||||
const resumeFile = ref(null); // 上传的简历文件
|
||||
const sessionId = ref(null); // 当前会话ID
|
||||
const messages = ref([]); // 对话消息列表
|
||||
const userAnswer = ref(''); // 用户输入框的内容
|
||||
const interviewStatus = ref('ACTIVE'); // 面试状态
|
||||
|
||||
// --- DOM引用 ---
|
||||
const messagesContainer = ref(null); // 消息容器的引用
|
||||
const uploadRef = ref(null); // 上传组件的引用
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
// 检查URL中是否有sessionId,用于恢复面试
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sid = urlParams.get('sessionId');
|
||||
if (sid) {
|
||||
fetchSessionHistory(sid);
|
||||
}
|
||||
});
|
||||
|
||||
// --- UI交互方法 ---
|
||||
const handleFileChange = (file) => {
|
||||
resumeFile.value = file.raw;
|
||||
};
|
||||
const handleFileExceed = () => {
|
||||
ElMessage.warning('只能上传一个简历文件。');
|
||||
};
|
||||
|
||||
// 滚动到消息列表底部
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化消息,将换行符转为<br>
|
||||
const formatMessage = (content) => content ? content.replace(/\n/g, '<br />') : '';
|
||||
|
||||
// --- API交互方法 ---
|
||||
|
||||
// 获取历史会话
|
||||
const fetchSessionHistory = async (sid) => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const responseData = await getInterviewReportDetail(sid);
|
||||
const data = responseData.data;
|
||||
sessionId.value = data.sessionDetails.sessionId;
|
||||
candidateName.value = data.sessionDetails.candidateName;
|
||||
interviewStatus.value = data.sessionDetails.status;
|
||||
// 注意:历史记录接口现在不直接返回messages,这里需要适配
|
||||
// 此处暂时留空,复盘报告页将展示完整对话
|
||||
messages.value = data.messages.map(msg => ({ sender: msg.sender, content: msg.content }));
|
||||
currentQuestionId.value = data.currentQuestionId
|
||||
interviewStarted.value = true;
|
||||
// 如果是已完成的面试,直接显示提示
|
||||
if (interviewStatus.value === 'COMPLETED') {
|
||||
messages.value.push({sender: 'AI', content: '本次面试已完成,详细报告请在“面试历史”页面查看。'});
|
||||
}
|
||||
scrollToBottom();
|
||||
} catch (error) {
|
||||
console.error('获取会话历史失败:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const currentQuestionId = ref('')
|
||||
|
||||
// 开始面试
|
||||
const startInterviewAction = async () => {
|
||||
if (!candidateName.value || !resumeFile.value) {
|
||||
ElMessage.error('请输入您的姓名并上传简历。');
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('candidateName', candidateName.value);
|
||||
formData.append('resume', resumeFile.value);
|
||||
try {
|
||||
console.log(formData.values())
|
||||
const responseData = await startInterview(formData);
|
||||
const data = responseData.data;
|
||||
sessionId.value = data.sessionId;
|
||||
currentQuestionId.value = data.currentQuestionId;
|
||||
messages.value.push({sender: 'AI', content: data.message});
|
||||
interviewStarted.value = true;
|
||||
window.history.pushState({}, '', `/interview?sessionId=${data.sessionId}`);
|
||||
scrollToBottom();
|
||||
} catch (error) {
|
||||
console.error('开始面试失败:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 发送回答
|
||||
const sendMessage = async () => {
|
||||
if (!userAnswer.value.trim()) return;
|
||||
const currentAnswer = userAnswer.value;
|
||||
messages.value.push({sender: 'USER', content: currentAnswer});
|
||||
userAnswer.value = '';
|
||||
isAiThinking.value = true;
|
||||
scrollToBottom();
|
||||
try {
|
||||
const responseData = await continueInterview(
|
||||
{
|
||||
sessionId: sessionId.value,
|
||||
userAnswer: currentAnswer,
|
||||
currentQuestionId: Number(currentQuestionId.value)
|
||||
}
|
||||
);
|
||||
const data = responseData.data;
|
||||
messages.value.push({sender: 'AI', content: data.message});
|
||||
interviewStatus.value = data.status;
|
||||
scrollToBottom();
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
messages.value.pop();
|
||||
userAnswer.value = currentAnswer;
|
||||
} finally {
|
||||
isAiThinking.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* --- 启动界面样式 --- */
|
||||
.start-screen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.start-card {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.start-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.start-illustration {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.start-form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.start-form h2 {
|
||||
font-size: 1.8em;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.start-form p {
|
||||
color: #606266;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* --- 聊天窗口样式 --- */
|
||||
.chat-window-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 140px); /* 减去顶栏和padding的高度 */
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message-ai {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
margin-right: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-user .avatar {
|
||||
order: 2;
|
||||
margin-right: 0;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 18px;
|
||||
border-radius: 18px;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-ai .message-bubble {
|
||||
background-color: #f0f2f5;
|
||||
color: #303133;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.message-user .message-bubble {
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* --- 输入区样式 --- */
|
||||
.input-area {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.answer-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.char-counter {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
280
src/views/QuestionBank.vue
Normal file
280
src/views/QuestionBank.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="question-bank-container">
|
||||
<el-card class="box-card" shadow="never">
|
||||
|
||||
<!-- 卡片头部:标题和操作按钮 -->
|
||||
<div class="card-header">
|
||||
<el-card style="width: 100%;" shadow="hover">
|
||||
<template #header>
|
||||
<span>题库管理中心</span>
|
||||
</template>
|
||||
<el-row :gutter="20" justify="space-between">
|
||||
<el-col :span="12">
|
||||
<el-input v-model="searchParams.content" placeholder="按题目内容搜索..." class="search-input"
|
||||
:prefix-icon="Search" @clear="fetchQuestionPage" @keyup.enter="fetchQuestionPage" clearable />
|
||||
<el-button type="primary" :icon="Search" @click="fetchQuestionPage"
|
||||
style="margin-left: 15px;">查询</el-button>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-button type="success" :icon="Plus" @click="handleOpenAddDialog">新增题目</el-button>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleFileUpload"
|
||||
class="upload-button">
|
||||
<el-button type="primary" :icon="UploadFilled">AI批量导入</el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="primary" @click="sendCheckDataReq">校验数据</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<!-- 题库表格 -->
|
||||
<el-table :data="tableData" border v-loading="isLoading" style="width: 100%" height="calc(100vh - 260px)">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="category" label="分类" width="180">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.category }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="difficulty" label="难度" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getDifficultyTagType(scope.row.difficulty)">{{ scope.row.difficulty }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="题目内容" show-overflow-tooltip />
|
||||
<el-table-column prop="tags" label="标签" width="250">
|
||||
<template #default="scope">
|
||||
<el-tag v-for="tag in (scope.row.tags || '').split(',').filter(t => t.trim() !== '')" :key="tag"
|
||||
type="info" style="margin-right: 5px; margin-bottom: 5px;">{{ tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" :icon="Edit" @click="handleOpenEditDialog(scope.row)"></el-button>
|
||||
<el-button type="danger" size="small" :icon="Delete" @click="handleDelete(scope.row.id)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页控制器 -->
|
||||
<el-pagination v-if="totalItems > 0" class="pagination-container"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="totalItems" :page-sizes="[10, 20, 50, 100]"
|
||||
v-model:current-page="pagination.current" v-model:page-size="pagination.size" @size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange" />
|
||||
</el-card>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑题目的对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%" @close="resetForm">
|
||||
<el-form :model="questionForm" ref="questionFormRef" label-width="80px">
|
||||
<el-form-item label="题目内容" prop="content" required>
|
||||
<el-input v-model="questionForm.content" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="category" required>
|
||||
<el-input v-model="questionForm.category" />
|
||||
</el-form-item>
|
||||
<el-form-item label="难度" prop="difficulty" required>
|
||||
<el-select v-model="questionForm.difficulty" placeholder="请选择难度">
|
||||
<el-option label="Easy" value="Easy" />
|
||||
<el-option label="Medium" value="Medium" />
|
||||
<el-option label="Hard" value="Hard" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签" prop="tags">
|
||||
<el-input v-model="questionForm.tags" placeholder="多个标签请用英文逗号分隔" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getQuestionPage, addQuestion, updateQuestion, deleteQuestion, importQuestionsByAi, checkQuestionData } from '../api/question';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search, UploadFilled, Plus, Edit, Delete } from '@element-plus/icons-vue';
|
||||
|
||||
// --- 响应式状态定义 ---
|
||||
const tableData = ref([]);
|
||||
const totalItems = ref(0);
|
||||
const isLoading = ref(false);
|
||||
const pagination = ref({ current: 1, size: 10 });
|
||||
const searchParams = ref({ content: '' });
|
||||
|
||||
// --- 对话框状态 ---
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
const isEditMode = ref(false);
|
||||
const questionForm = ref({});
|
||||
const questionFormRef = ref(null);
|
||||
|
||||
// --- UI辅助方法 ---
|
||||
const getDifficultyTagType = (difficulty) => {
|
||||
switch ((difficulty || '').toLowerCase()) {
|
||||
case 'easy':
|
||||
return 'success';
|
||||
case 'medium':
|
||||
return 'warning';
|
||||
case 'hard':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
questionForm.value = { content: '', category: '', difficulty: 'Medium', tags: '' };
|
||||
isEditMode.value = false;
|
||||
};
|
||||
|
||||
// --- 对话框处理方法 ---
|
||||
const handleOpenAddDialog = () => {
|
||||
resetForm();
|
||||
dialogTitle.value = '新增题目';
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleOpenEditDialog = (row) => {
|
||||
resetForm();
|
||||
isEditMode.value = true;
|
||||
dialogTitle.value = '编辑题目';
|
||||
questionForm.value = { ...row };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// --- API交互方法 ---
|
||||
const fetchQuestionPage = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
current: pagination.value.current,
|
||||
size: pagination.value.size,
|
||||
...searchParams.value
|
||||
};
|
||||
const responseData = await getQuestionPage(params);
|
||||
tableData.value = responseData.data.records;
|
||||
totalItems.value = responseData.data.total;
|
||||
} catch (error) {
|
||||
console.error('获取题库分页失败:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const sendCheckDataReq = async () => {
|
||||
const res = await checkQuestionData()
|
||||
console.log(res)
|
||||
|
||||
}
|
||||
|
||||
const handleFileUpload = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.raw);
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await importQuestionsByAi(formData);
|
||||
ElMessage.success('文件上传成功!AI正在后台处理,请稍后刷新查看。');
|
||||
await fetchQuestionPage()
|
||||
// setTimeout(fetchQuestionPage, 3000);
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isEditMode.value) {
|
||||
await updateQuestion(questionForm.value);
|
||||
ElMessage.success('题目更新成功!');
|
||||
} else {
|
||||
await addQuestion(questionForm.value);
|
||||
ElMessage.success('题目新增成功!');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
fetchQuestionPage();
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
ElMessageBox.confirm('确定要删除这道题目吗?此操作不可撤销。', '警告', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteQuestion(id);
|
||||
ElMessage.success('题目删除成功!');
|
||||
fetchQuestionPage();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessage.info('已取消删除');
|
||||
});
|
||||
};
|
||||
|
||||
// --- 分页处理 ---
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.value.size = val;
|
||||
fetchQuestionPage();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.value.current = val;
|
||||
fetchQuestionPage();
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
fetchQuestionPage();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.question-bank-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
11
src/views/question-category/index.vue
Normal file
11
src/views/question-category/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
18
vite.config.js
Normal file
18
vite.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
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
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user