Compare commits

...

No commits in common. 'master' and 'daijiajun/feature-20251107115823-股票知识测评' have entirely different histories.

  1. 28
      .gitignore
  2. 24
      README.md
  3. 5
      babel.config.js
  4. 19
      jsconfig.json
  5. 19804
      package-lock.json
  6. 46
      package.json
  7. BIN
      public/favicon.ico
  8. 17
      public/index.html
  9. 155
      src/App.vue
  10. 18
      src/api/question.js
  11. 32
      src/api/userStatistics.js
  12. 19
      src/api/wrongQuestion.js
  13. BIN
      src/assets/logo.png
  14. BIN
      src/assets/screenshot.jpg
  15. 36
      src/components/Layout/Header.vue
  16. 60
      src/components/Layout/Sidebar.vue
  17. 465
      src/components/Question/QuestionSearch.vue
  18. 747
      src/components/Question/QuestionTable.vue
  19. 38
      src/components/Tabs/TabNavigation.vue
  20. 221
      src/components/UserStatistics/UserStatisticsSearch.vue
  21. 557
      src/components/UserStatistics/UserStatisticsTable.vue
  22. 125
      src/components/WrongQuestion/WrongQuestionSearch.vue
  23. 534
      src/components/WrongQuestion/WrongQuestionTable.vue
  24. 23
      src/main.js
  25. 49
      src/router/index.js
  26. 71
      src/views/MainPage.vue
  27. 40
      src/views/QuestionManage.vue
  28. 72
      src/views/UserStatistics.vue
  29. 49
      src/views/WrongQuestion.vue
  30. 4
      vue.config.js

28
.gitignore

@ -1,11 +1,23 @@
# ---> Vue
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
.DS_Store
node_modules
/dist
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
README.md

@ -1,2 +1,24 @@
# Knowledge_Test_Vue
# evaluation-system
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
babel.config.js

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
jsconfig.json

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

19804
package-lock.json
File diff suppressed because it is too large
View File

46
package.json

@ -0,0 +1,46 @@
{
"name": "evaluation-system",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.13.2",
"core-js": "^3.8.3",
"element-ui": "^2.15.14",
"vue": "^2.6.14",
"vue-router": "^3.6.5"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
public/favicon.ico

17
public/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

155
src/App.vue

@ -0,0 +1,155 @@
<template>
<div id="app">
<Header />
<div class="container">
<Sidebar />
<div class="content">
<div class="title">测评系统后台</div>
<TabNavigation />
<router-view />
</div>
</div>
</div>
</template>
<script>
import Header from './components/Layout/Header.vue'
import Sidebar from './components/Layout/Sidebar.vue'
import TabNavigation from './components/Tabs/TabNavigation.vue'
export default {
name: 'App',
components: {
Header,
Sidebar,
TabNavigation
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
}
#app {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
height: 60px;
background-color: white;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.logo {
color: #e74c3c;
font-weight: bold;
font-size: 18px;
}
.container {
display: flex;
flex: 1;
overflow: hidden;
}
.content {
flex: 1;
padding: 20px;
background-color: white;
overflow-y: auto;
position: relative;
}
.title {
font-size: 20px;
margin-bottom: 20px;
color: #333;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-btn {
padding: 8px 16px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.3s;
}
.tab-btn:hover {
background-color: #c0392b;
}
.tab-btn.active {
opacity: 0.5;
}
.search-area {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 20px;
}
.search-item {
display: flex;
flex-direction: column;
min-width: 160px;
}
.search-item label {
margin-bottom: 5px;
font-size: 14px;
color: #666;
}
.search-item select,
.search-item input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.btn-group {
display: flex;
gap: 10px;
}
.btn-red {
padding: 8px 16px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-red.small {
padding: 4px 10px;
font-size: 12px;
}
</style>

18
src/api/question.js

@ -0,0 +1,18 @@
// src/api/question.js
import axios from 'axios'
const api = axios.create({
baseURL: 'http://192.168.40.41:8000/admin', // 修改这里
timeout: 5000
})
export const getQuestions = (params) => {
return api.post('/questions/get', params) // 确保这里的路径正确
}
// export const updateQuestion = (params) => {
// return api.post('/questions/update', params) // 新增更新题目的接口
// }
export const deleteQuestion = (params) => {
return api.post('/questions/del', params) // 新增删除题目的接口
}

32
src/api/userStatistics.js

@ -0,0 +1,32 @@
// src/api/userStatistics.js
import axios from 'axios'
const adminApi = axios.create({
baseURL: 'http://192.168.40.41:8000/admin',
timeout: 5000
})
const apiApi = axios.create({
baseURL: 'http://192.168.40.41:8000/api', // 为 API 接口创建独立实例
timeout: 5000
})
export const getUserStatistics = async (params) => {
try {
const response = await adminApi.post('/user/score', params)
return response.data
} catch (error) {
console.error('获取用户统计数据失败:', error)
throw error
}
}
export const getWrongQuestions = async (params) => {
try {
const response = await apiApi.post('/knowledge/wrong-questions', params)
return response.data
} catch (error) {
console.error('获取用户错题失败:', error)
throw error
}
}

19
src/api/wrongQuestion.js

@ -0,0 +1,19 @@
// src/api/wrongQuestion.js
import axios from 'axios'
const api = axios.create({
baseURL: 'http://192.168.40.41:8000/admin',
timeout: 5000
})
export const getQuestions = (params) => {
return api.post('/questions/get', params)
}
export const getUsersByQuestionId = (id) => {
return api.post('/questions/user', { id }, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}

BIN
src/assets/logo.png

After

Width: 200  |  Height: 200  |  Size: 6.7 KiB

BIN
src/assets/screenshot.jpg

After

Width: 1361  |  Height: 854  |  Size: 243 KiB

36
src/components/Layout/Header.vue

@ -0,0 +1,36 @@
<!--头部组件-->
<template>
<!-- 头部容器 -->
<div class="header">
<!-- 系统Logo -->
<div class="logo">Homily Link</div>
</div>
</template>
<script>
export default {
// Header
// eslint-disable-next-line vue/multi-word-component-names
name: 'Header'
}
</script>
<style scoped>
/* 头部样式 */
.header {
height: 60px; /* 高度60px */
background-color: white; /* 白色背景 */
display: flex; /* 使用flex布局 */
align-items: center; /* 垂直居中对齐 */
justify-content: flex-start; /* 内容靠左对齐 */
padding-left: 20px; /* 左内边距20px */
box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* 添加底部阴影效果 */
}
/* Logo样式 */
.logo {
color: #e74c3c; /* 红色字体 */
font-weight: bold; /* 粗体 */
font-size: 18px; /* 字体大小18px */
}
</style>

60
src/components/Layout/Sidebar.vue

@ -0,0 +1,60 @@
<!--侧边栏组件-->
<template>
<!-- 侧边栏容器 -->
<div class="sidebar">
<!-- 侧边栏菜单列表 -->
<ul class="sidebar-menu">
<!-- 各个菜单项 -->
<li>主页</li>
<li>发文章</li>
<li>我的内容</li>
<li>我的收藏</li>
<li>评论管理</li>
<li>账号设置</li>
<!-- 当前激活的菜单项 -->
<li class="active">测评管理</li>
</ul>
</div>
</template>
<script>
export default {
// SideBar
name: 'SideBar'
}
</script>
<style scoped>
/* 侧边栏整体样式 */
.sidebar {
width: 200px; /* 宽度200px */
background-color: #f5f5f5; /* 背景色 */
height: 100%; /* 高度占满父容器 */
border-right: 1px solid #ddd; /* 右边框 */
}
/* 侧边栏菜单样式 */
.sidebar-menu {
list-style: none; /* 移除默认列表样式 */
padding: 0; /* 清除内边距 */
margin: 0; /* 清除外边距 */
}
/* 菜单项通用样式 */
.sidebar-menu li {
padding: 15px 20px; /* 内边距 */
cursor: pointer; /* 鼠标指针样式 */
transition: background-color 0.3s; /* 背景色过渡动画 */
}
/* 菜单项悬停效果 */
.sidebar-menu li:hover {
background-color: #e0e0e0; /* 悬停时的背景色 */
}
/* 激活状态的菜单项样式 */
.sidebar-menu li.active {
background-color: #d1ecf1; /* 激活项背景色 */
color: #007bff; /* 激活项文字颜色 */
}
</style>

465
src/components/Question/QuestionSearch.vue

@ -0,0 +1,465 @@
<!-- src/components/Question/QuestionSearch.vue -->
<template>
<div class="question-search-container">
<div class="top">
<h2>题库管理</h2>
</div>
<!-- 搜索区域容器 -->
<div class="search-area">
<!-- 题目类型筛选项 -->
<div class="search-item">
<h3>题目类型</h3>
<select v-model="searchForm.questionType">
<option value="">全部</option>
<option>股票知识</option>
<option>企业文化</option>
</select>
</div>
<!-- 题干关键词搜索项 -->
<div class="search-item">
<h3>题干查找</h3>
<input type="text" placeholder="请输入题干关键词" v-model="searchForm.keyword" />
</div>
<!-- 课程推荐筛选项 -->
<div class="search-item">
<h3>推荐系列</h3>
<select v-model="searchForm.course">
<option value="">全部</option>
<option>量能擒牛</option>
<option>价格破译</option>
<option>量价时空综合</option>
</select>
</div>
<!-- 操作按钮组 -->
<div class="btn-group">
<button class="btn-red" @click="handleSearch">查找</button>
<button class="btn-red" @click="showAddModal = true">新增题目</button>
<button class="btn-red" @click="exportExcel">Excel导出</button>
</div>
</div>
<!-- 新增题目弹窗 -->
<div v-if="showAddModal" class="modal-overlay">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>新增题目</h3>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<div class="form-row">
<label>题目类型</label>
<select v-model="newQuestion.questionTypeName">
<option value="股票知识">股票知识</option>
<option value="企业文化">企业文化</option>
</select>
</div>
<div class="form-row">
<label>题干</label>
<textarea v-model="newQuestion.stem" placeholder="请输入题目内容" rows="4"
style="width: 545px; height: 120px;"
></textarea>
</div>
<div class="form-row-options">
<div class="option-group">
<label>选项A</label>
<input
type="text"
v-model="newQuestion.optionA"
placeholder="请输入选项A"
style="width: 280px; height: 40px;"
/>
</div>
<div class="option-group">
<label>选项B</label>
<input type="text" v-model="newQuestion.optionB" placeholder="请输入选项B" style="width: 280px; height: 40px;"/>
</div>
<div class="option-group">
<label>选项C</label>
<input type="text" v-model="newQuestion.optionC" placeholder="请输入选项C" style="width: 280px; height: 40px;"/>
</div>
<div class="option-group">
<label>选项D</label>
<input type="text" v-model="newQuestion.optionD" placeholder="请输入选项D" style="width: 280px; height: 40px;"/>
</div>
</div>
<div class="form-row">
<label>正确答案</label>
<select v-model="newQuestion.correctAnswer">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
<div class="form-row">
<label>推荐系列</label>
<select v-model="newQuestion.recommendedCourse">
<option value="量能擒牛">量能擒牛</option>
<option value="价格破译">价格破译</option>
<option value="量价时空综合">量价时空综合</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn-red" @click="addQuestion">确定</button>
<button class="btn-red" @click="closeModal">取消</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getQuestions } from '@/api/question.js';
import axios from 'axios';
// import { Message } from 'element-ui'
export default {
name: 'QuestionSearch',
data() {
return {
searchForm: {
questionType: '',
keyword: '',
course: ''
},
showAddModal: false,
newQuestion: {
id: 0,
stem: '',
optionA: '',
optionB: '',
optionC: '',
optionD: '',
correctAnswer: 'A',
questionTypeName: '股票知识',
recommendedCourse: '量能擒牛'
},
currentPage: 1, //
total: 0 //
};
},
methods: {
async handleSearch(page = 1) {
try {
this.currentPage = page; //
const params = new URLSearchParams();
params.append('page', page); // 使
params.append('page_size', 20);
// id
const questionTypeIdMap = {
'股票知识': 1,
'企业文化': 2
};
if (this.searchForm.questionType) {
params.append('question_type_id', questionTypeIdMap[this.searchForm.questionType]);
}
// id
const courseRecommendationIdMap = {
'量能擒牛': 1,
'价格破译': 2,
'量价时空综合': 3
};
if (this.searchForm.course) {
params.append('course_recommendation_id', courseRecommendationIdMap[this.searchForm.course]);
}
//
if (this.searchForm.keyword) {
params.append('stem', this.searchForm.keyword);
}
const response = await getQuestions(params);
// if (response.data.code === 200) {
// // 便
// const resultData = {
// list: response.data.data.list,
// total: response.data.data.total || []
// };
// this.$emit('search-result', resultData);
// this.total = response.data.data.total || 0;
// } else {
// alert('' + response.data.msg);
// }
if (response.data.code === 200) {
const list = response.data.data.list || [];
const totalRaw = response.data.data.total;
const total = Number.isFinite(Number(totalRaw)) ? Number(totalRaw) : 1;
const resultData = { list, total };
this.$emit('search-result', resultData);
this.total = total;
} else {
alert('搜索失败:' + response.data.msg);
}
} catch (error) {
console.error('搜索失败:', error);
alert('网络错误,请检查连接!');
}
},
async addQuestion() {
//
if (!this.newQuestion.stem || !this.newQuestion.optionA || !this.newQuestion.optionB ||
!this.newQuestion.optionC || !this.newQuestion.optionD || !this.newQuestion.correctAnswer || !this.newQuestion.recommendedCourse) {
alert('请填写所有必填项!');
return;
}
try {
//
const params = new URLSearchParams();
params.append('id', this.newQuestion.id);
params.append('stem', this.newQuestion.stem);
params.append('A', this.newQuestion.optionA);
params.append('B', this.newQuestion.optionB);
params.append('C', this.newQuestion.optionC);
params.append('D', this.newQuestion.optionD);
params.append('correct_answer', this.newQuestion.correctAnswer);
params.append('question_type_id', this.newQuestion.questionTypeName === '股票知识' ? 1 : 2);
params.append('course_recommendation_id',
this.newQuestion.recommendedCourse === '量能擒牛' ? 1 :
this.newQuestion.recommendedCourse === '价格破译' ? 2 : 3 );
//
// const response = await axios.post('/admin/questions/update', params, {
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// }
// });
//
console.log(params);
const response = await axios.post('http://192.168.40.41:8000/admin/questions/update',params,
{
headers: {
'Content-Type': 'application/json'
}
});
console.log(response.data);
if (response.data.code === 200) {
this.closeModal();
console.log('第二步');
this.$emit('question-added');
console.log('第三步');
this.$message({
message: '添加题目成功!',
type: 'success'
});
console.log('第四步');
} else {
alert('添加题目失败:' + response.data.msg);
}
} catch (error) {
console.error('添加题目失败:', error);
alert('网络错误,请检查连接!');
}
},
//
async handlePageChange(page) {
await this.handleSearch(page);
},
async exportExcel() {
try{
//
const exportParams = {};
//
const questionTypeIdMap = {
'股票知识': 1,
'企业文化': 2
};
if (this.searchForm.questionType) {
exportParams.question_type_id = questionTypeIdMap[this.searchForm.questionType];
}
//
const courseRecommendationIdMap = {
'量能擒牛': 1,
'价格破译': 2,
'量价时空综合': 3
};
if (this.searchForm.course) {
exportParams.course_recommendation_id = courseRecommendationIdMap[this.searchForm.course];
}
//
if (this.searchForm.keyword) {
exportParams.stem = this.searchForm.keyword;
}
//
const response = await axios.post(
'http://192.168.40.41:8000/admin/questions/export',
exportParams,
{ responseType: 'blob' }
);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', '题库详细数据表.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
alert('导出成功!');
}catch (error) {
console.error('导出 Excel 失败:', error);
alert('网络错误,请检查连接!');
}
},
closeModal() {
this.showAddModal = false;
console.log('关闭弹窗 第一步');
},
resetForm() {
this.newQuestion = {
id: 0,
stem: '',
optionA: '',
optionB: '',
optionC: '',
optionD: '',
correctAnswer: 'A',
questionTypeName: '股票知识',
recommendedCourse: '量能擒牛'
};
}
}
};
</script>
<style scoped>
.top{
padding: 20px 0px;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 8px;
width: 620px;
height: 760px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 5px;
border-radius: 50%;
transition: color 0.2s;
}
.close-btn:hover {
color: #e74c3c;
}
.modal-body {
padding: 20px;
max-height: 600px;
overflow-y: auto;
}
.form-row {
margin-bottom: 16px;
}
.form-row label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-row select,
.form-row input[type="text"],
.form-row textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.form-row textarea {
resize: vertical;
}
.form-row-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.option-group {
display: flex;
flex-direction: column;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 16px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.modal-content {
width: 90%;
}
.form-row-options {
grid-template-columns: 1fr;
}
}
</style>

747
src/components/Question/QuestionTable.vue

@ -0,0 +1,747 @@
<!-- src/components/Question/QuestionTable.vue -->
<template>
<div class="question-table-container">
<div class="table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>题干</th>
<th>题目类型</th>
<th>出错次数</th>
<th>出错率</th>
<th>推荐系列</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in items" :key="item.id" :data-id="item.id">
<td>{{ (page- 1) * pageSize + index + 1 }}</td>
<td>{{ item.stem }}</td>
<td>{{ item.questionTypeName }}</td>
<td>{{ item.errorCount }}</td>
<td>{{ item.errorRate }}%</td>
<td>{{ item.CrName }}</td>
<td>
<button class="btn-red small" @click="viewQuestion(item)">查看</button>
<button class="btn-red small" @click="editQuestion(item)">修改</button>
<button class="btn-red small" @click="deleteQuestion(item)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination-container">
<div class="pagination-info">
{{ total }} 条记录 {{ page }}
</div>
<div class="pagination-controls">
<button
class="btn-pagination"
:disabled="page <= 1"
@click="changePage(1)"
>
首页
</button>
<button
class="btn-pagination"
:disabled="page <= 1"
@click="changePage(page - 1)"
>
上一页
</button>
<input type="number" class="page-input" :value="page" @keyup.enter="jumpToPage" min="1" :max="totalPages"/>
<span class="page-info">/ {{ totalPages }}</span>
<button class="btn-pagination" :disabled="page >= totalPages" @click="changePage(page + 1)">
下一页
</button>
<button class="btn-pagination" :disabled="page >= totalPages" @click="changePage(totalPages)">
尾页
</button>
</div>
</div>
<div v-if="showViewModal" class="modal-overlay" @click.self="closeViewModal">
<div class="view-modal-content" @click.stop>
<div class="modal-header">
<h3>题目详情</h3>
<button class="close-btn" @click="closeViewModal">×</button>
</div>
<div class="view-modal-body">
<p class="question-text">{{ currentQuestion.stem }}</p>
<div class="options-container">
<div v-for="option in ['A', 'B', 'C', 'D']" :key="option" :class="[option === currentQuestion.correctAnswer ? 'option-item-correct' : 'option-item']">
{{ `${option}. ${currentQuestion[`option${option}`]}` }}
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-red" @click="closeViewModal">退出</button>
</div>
</div>
</div>
<div class="footer-btn">
<button class="btn-red">应用</button>
</div>
<div v-show="showEditModal" class="modal-overlay" @click.self="closeEditModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>编辑题目</h3>
<button class="close-btn" @click="closeEditModal">×</button>
</div>
<div class="modal-body">
<div class="form-row">
<label>题目类型</label>
<select v-model="editingQuestion.questionTypeName">
<option value="股票知识">股票知识</option>
<option value="企业文化">企业文化</option>
</select>
</div>
<div class="form-row">
<label>题干</label>
<textarea v-model="editingQuestion.stem" placeholder="请输入题目内容" rows="4" style="width: 545px; height: 120px;"
></textarea>
</div>
<div class="form-row-options">
<div class="option-group">
<label>选项A</label>
<input type="text" v-model="editingQuestion.optionA" placeholder="请输入选项A" style="width: 280px; height: 40px;"/>
</div>
<div class="option-group">
<label>选项B</label>
<input type="text" v-model="editingQuestion.optionB" placeholder="请输入选项B" style="width: 280px; height: 40px;"/>
</div>
<div class="option-group">
<label>选项C</label>
<input type="text" v-model="editingQuestion.optionC" placeholder="请输入选项C" style="width: 280px; height: 40px;"/>
</div>
<div class="option-group">
<label>选项D</label>
<input type="text" v-model="editingQuestion.optionD" placeholder="请输入选项D" style="width: 280px; height: 40px;"/>
</div>
</div>
<div class="form-row">
<label>正确答案</label>
<select v-model="editingQuestion.correctAnswer">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
<div class="form-row">
<label>推荐课程</label>
<select v-model="editingQuestion.recommendedCourse">
<option value="量能擒牛">量能擒牛</option>
<option value="价格破译">价格破译</option>
<option value="量价时空综合">量价时空综合</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn-red" @click="updateQuestion">确定</button>
<button class="btn-red" @click="closeEditModal">取消</button>
</div>
</div>
</div>
<div v-if="showDeleteModal" class="modal-overlay" @click.self="closeDeleteModal">
<div class="delete-modal-content" @click.stop>
<div class="modal-header">
<h3>您确定要删除吗</h3>
<button class="close-btn" @click="closeDeleteModal">×</button>
</div>
<div class="modal-footer">
<button class="btn-red" @click="confirmDelete">确定</button>
<button class="btn-red" @click="closeDeleteModal">取消</button>
</div>
</div>
</div>
</div>
</template>
<script>
// API
import {deleteQuestion, getQuestions} from '@/api/question.js'
import axios from "axios";
export default {
name: 'QuestionTable',
data() {
return {
items: [], //
showViewModal: false, //
currentQuestion: {}, //
page: 1, //
pageSize: 20, //
total: 0, //
showEditModal: false, //
editingQuestion: {}, //
showDeleteModal: false, //
deleteId: null, // ID
}
},
computed: {
//
totalPages() {
if (this.total === 0) {
return 1;
}
return Math.ceil(this.total / this.pageSize);
}
},
async mounted() {
//
await this.fetchQuestions()
},
methods: {
//
setData(data, resetPage = false) {
this.items = data.list || data;
this.total = data.total || (data.list ? data.list.length : data.length);
if (resetPage || this.page > this.totalPages) {
this.page = 1;
}
},
//
async fetchQuestions() {
try {
//
const params = {
Page: this.page,
PageSize: this.pageSize
}
// API
const response = await getQuestions(params)
//
if (response.data.code === 200) {
this.items = response.data.data.list
this.total = response.data.data.total !== 0 ? response.data.data.total : 1
}
} catch (error) {
console.error('获取题目数据失败:', error)
}
},
//
changePage(newPage) {
if (newPage >= 1 && newPage <= this.totalPages) {
this.page = newPage;
//
this.$emit('page-changed', newPage);
}
},
//
jumpToPage(event) {
const targetPage = parseInt(event.target.value);
if (targetPage >= 1 && targetPage <= this.totalPages) {
this.page = targetPage;
//
this.$emit('page-changed', targetPage);
} else {
//
event.target.value = this.page;
}
},
//
async viewQuestion(item) {
// 1.
const row = document.querySelector(`[data-id="${item.id}"]`);
if (row) {
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// 2. API
try {
// ID
const params = {
Page: 1,
PageSize: 100, //
id: item.id
};
const response = await getQuestions(params);
if (response.data.code === 200 && response.data.data.list && response.data.data.list.length > 0) {
// ID使
const rawQuestion = response.data.data.list.find(question => question.id === item.id);
if (!rawQuestion) {
alert('未找到该题目!');
return;
}
//
this.currentQuestion = {
id: rawQuestion.id,
stem: rawQuestion.stem,
optionA: rawQuestion.A,
optionB: rawQuestion.B,
optionC: rawQuestion.C,
optionD: rawQuestion.D,
correctAnswer: rawQuestion.correctAnswer,
questionTypeName: rawQuestion.questionTypeName,
CrName: rawQuestion.CrName
};
//
this.showViewModal = true;
} else {
alert('未找到该题目!');
}
} catch (error) {
console.error('获取题目详情失败:', error);
alert('网络错误,请检查连接!');
}
},
//
closeViewModal() {
this.showViewModal = false;
},
//
editQuestion(item) {
//
this.editingQuestion = {
id: item.id,
stem: item.stem,
optionA: item['A'],
optionB: item['B'],
optionC: item['C'],
optionD: item['D'],
correctAnswer: item.correctAnswer,
questionTypeName: item.questionTypeName,
recommendedCourse: item.CrName
};
//
this.showEditModal = true;
},
//
async updateQuestion() {
//
if (!this.editingQuestion.stem || !this.editingQuestion.optionA ||
!this.editingQuestion.optionB || !this.editingQuestion.optionC ||
!this.editingQuestion.optionD || !this.editingQuestion.correctAnswer) {
alert('请填写所有必填项!');
return;
}
try {
//
const params = new URLSearchParams();
params.append('id', this.editingQuestion.id);
params.append('stem', this.editingQuestion.stem);
params.append('A', this.editingQuestion.optionA);
params.append('B', this.editingQuestion.optionB);
params.append('C', this.editingQuestion.optionC);
params.append('D', this.editingQuestion.optionD);
params.append('correct_answer', this.editingQuestion.correctAnswer);
params.append('question_type_id', this.editingQuestion.questionTypeName === '股票知识' ? 1 : 2);
params.append('course_recommendation_id',
this.editingQuestion.recommendedCourse === '量能擒牛' ? 1 :
this.editingQuestion.recommendedCourse === '价格破译' ? 2 : 3);
//
const response = await axios.post('http://192.168.40.41:8000/admin/questions/update',params,
{
headers: {
'Content-Type': 'application/json'
}
});
if (response.data.code === 200) {
this.closeEditModal();
//
await this.fetchQuestions();
this.$message({
message: '修改题目成功!',
type: 'success'
});
} else {
alert('修改题目失败:' + response.data.msg);
}
} catch (error) {
console.error('修改题目失败:', error);
alert('网络错误,请检查连接!');
}
},
//
closeEditModal() {
this.showEditModal = false;
this.editingQuestion = {};
},
//
async deleteQuestion(item) {
this.deleteId = item.id;
this.showDeleteModal = true;
},
//
async confirmDelete() {
try {
const response = await deleteQuestion({ id: this.deleteId })
if (response.data.code === 200) {
this.closeDeleteModal()
await this.fetchQuestions()
this.$message({
message: '删除成功!',
type: 'success'
});
} else {
alert('删除失败:' + response.data.msg)
}
} catch (error) {
console.error('删除失败:', error)
alert('网络错误,请检查连接!')
}
},
//
closeDeleteModal() {
this.showDeleteModal = false;
this.deleteId = null;
}
}
}
</script>
<style scoped>
/* 修改操作列按钮间距 */
td:last-child {
display: flex;
gap: 16px; /* 将间距设置为16px */
}
th {
display: table-cell !important;
vertical-align: middle !important;
}
.form-row-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-row {
margin-bottom: 16px;
}
.form-row label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-row select,
.form-row input[type="text"],
.form-row textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.form-row textarea {
resize: vertical;
}
.option-group {
display: flex;
flex-direction: column;
}
/* 响应式设计 */
@media (max-width: 768px) {
.modal-content {
width: 90%;
}
.form-row-options {
grid-template-columns: 1fr;
}
}
/* 表格容器样式 */
.table-container {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
/* 表格样式 */
table {
width: 100%;
border-collapse: collapse;
background-color: white;
}
/* 表格单元格样式 */
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
/* 表头样式 */
th {
background-color: #f2f2f2;
font-weight: normal;
color: #333;
}
/* 表格行悬停效果 */
tr:hover {
background-color: #f9f9f9;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
/* 查看弹窗专用样式 */
.view-modal-content {
background-color: white;
border-radius: 8px;
width: 750px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.view-modal-body {
padding: 20px;
max-height: 600px;
overflow-y: auto;
}
/* 删除确认对话框样式 */
.delete-modal-content {
background-color: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.delete-modal-content .modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.delete-modal-content .modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.delete-modal-content .close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 5px;
border-radius: 50%;
transition: color 0.2s;
}
.delete-modal-content .close-btn:hover {
color: #e74c3c;
}
.delete-modal-content .modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 16px;
}
.modal-content {
background-color: white;
border-radius: 8px;
width: 600px;
height: 760px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 5px;
border-radius: 50%;
transition: color 0.2s;
}
.close-btn:hover {
color: #e74c3c;
}
.modal-body {
padding: 20px;
max-height: 600px;
overflow-y: auto;
}
.question-text {
font-size: 16px;
margin-bottom: 20px;
line-height: 1.5;
}
.options-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.option-item {
padding: 10px;
border-radius: 6px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
transition: all 0.2s;
}
.option-item-correct {
padding: 10px;
border-radius: 6px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
transition: all 0.2s;
background-color: #dc3545 !important;
color: white !important;
border-color: #c82333 !important;
font-weight: bold;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 16px;
}
/* 分页样式 */
.pagination-container {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 20px;
padding: 0 10px;
}
.pagination-info {
font-size: 14px;
color: #666;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 10px;
margin-right: 20px;
}
.btn-pagination {
padding: 6px 12px;
background-color: #f2f2f2;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-pagination:hover:not(:disabled) {
background-color: #e0e0e0;
}
.btn-pagination:disabled {
background-color: #f5f5f5;
color: #ccc;
cursor: not-allowed;
}
.page-input {
width: 50px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-size: 14px;
}
.page-info {
font-size: 14px;
color: #666;
}
.footer-btn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
</style>

38
src/components/Tabs/TabNavigation.vue

@ -0,0 +1,38 @@
<template>
<!-- 标签页导航容器 -->
<div class="tabs">
<!-- 用户数据标签按钮 -->
<button
class="tab-btn"
:class="{ active: $route.path === '/users' }"
@click="$router.push('/users')"
>
用户数据
</button>
<!-- 错题统计标签按钮 -->
<button
class="tab-btn"
:class="{ active: $route.path === '/wrong-questions' }"
@click="$router.push('/wrong-questions')"
>
错题统计
</button>
<!-- 题库管理标签按钮 -->
<button
class="tab-btn"
:class="{ active: $route.path === '/' }"
@click="$router.push('/')"
>
题库管理
</button>
</div>
</template>
<script>
export default {
//
name: 'TabNavigation'
}
</script>

221
src/components/UserStatistics/UserStatisticsSearch.vue

@ -0,0 +1,221 @@
<!-- UserStatisticsSearch.vue -->
<template>
<div>
<div class="top">
<h2>用户数据</h2>
</div>
<div class="search-area">
<!-- 题目类型 -->
<div class="search-item">
<h3>题目类型</h3>
<select v-model="filters.type">
<option value="">全部</option>
<option value="股票知识">股票知识</option>
</select>
</div>
<!-- 时间选择 -->
<div class="search-item date-range">
<h3>时间选择</h3>
<div class="date-input-wrapper">
<div class="date-box">
<input type="datetime-local" v-model="filters.startDate" step="1" />
</div>
<span class="separator"></span>
<div class="date-box">
<input type="datetime-local" v-model="filters.endDate" step="1" />
</div>
</div>
</div>
<!-- 用户名称 -->
<div class="search-item">
<h3>用户名称</h3>
<input type="text" v-model="filters.userName" placeholder="请输入用户名称" />
</div>
<!-- 精网号 -->
<div class="search-item">
<label>精网号</label>
<input type="text" v-model="filters.jingwangId" placeholder="请输入精网号" />
</div>
<!-- 身份 -->
<div class="search-item">
<label>身份</label>
<select v-model="filters.user_identity">
<option value="">全部</option>
<option value="非网">非网</option>
<option value="半年">半年</option>
<option value="终身">终身</option>
</select>
</div>
<!-- 操作按钮 -->
<div class="btn-group">
<button class="btn-red" @click="searchUserStatistics">查找</button>
<button class="btn-red" @click="exportToExcel">Excel导出</button>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'UserStatisticsSearch',
data() {
return {
filters: {
type: '',
startDate: '',
endDate: '',
userName: '',
jingwangId: '',
user_identity: ''
}
}
},
methods: {
searchUserStatistics() {
//
this.$emit('search', { ...this.filters });
},
async exportToExcel() {
try{
const params = new URLSearchParams();
const questionTypeIdMap = {
'股票知识':1,
'企业文化':2
};
//
if (this.filters.type) {
params.append('question_type_id', questionTypeIdMap[this.filters.type]);
}
//
if (this.filters.startDate) {
params.append('start_time', this.filters.startDate);
}
if (this.filters.endDate) {
params.append('end_time', this.filters.endDate);
}
if(this.filters.startDate>this.filters.endDate){
alert('开始时间不能晚于结束时间');
return;
}
//
if (this.filters.userName) {
params.append('user_name', this.filters.userName);
}
//
if (this.filters.jingwangId) {
params.append('jingwang_id', this.filters.jingwangId);
}
//
if (this.filters.user_identity) {
params.append('user_identity', this.filters.user_identity);
}
console.log(params);
const response = await axios.post('http://192.168.40.41:8000/admin/user/export',params,{responseType: 'blob'});
console.log(response);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', '用户统计数据表.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
alert('导出成功!');
}catch (error) {
console.error('导出 Excel 失败:', error);
alert('导出失败!');
}
}
}
}
</script>
<style scoped>
.search-area {
display: flex;
flex-wrap: wrap;
gap: 20px;
background-color: white;
border-radius: 4px;
}
.search-item {
display: flex;
flex-direction: column;
min-width: 100px;
}
.search-item label {
margin-bottom: 5px;
font-weight: bold;
}
.search-item input,
.search-item select {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn-group {
display: flex;
align-items: flex-end;
}
.btn-red {
background-color: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.btn-red:hover {
background-color: #c82333;
}
/* 时间选择器特殊样式 */
.date-range {
/* 设置为弹性盒子布局,垂直方向排列 */
display: flex;
flex-direction: column;
/* 固定宽度为344px */
width: 344px;
/* 固定高度为58px */
height: 58px;
}
.date-input-wrapper {
display: flex;
align-items: center;
width: 100%;
margin-top: 2px;
}
.date-box {
flex: 1;
}
.date-box input[type="date"] {
width: 100%;
height: 34px;
}
.separator {
margin: 0 10px;
white-space: nowrap;
}
</style>

557
src/components/UserStatistics/UserStatisticsTable.vue

@ -0,0 +1,557 @@
<!-- UserStatisticsTable.vue -->
<template>
<div class="table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名称</th>
<th>用户身份</th>
<th>精网号</th>
<th>题目类型</th>
<th>得分</th>
<th>提交时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in users" :key="user.id">
<td>{{ (page - 1) * pageSize + index + 1 }}</td>
<td>{{ user.user_name }}</td>
<td>{{ user.user_identity }}</td>
<td>{{ user.jwcode }}</td>
<td>股票知识</td>
<td>{{ user.score }}</td>
<td>{{ user.createdAt }}</td>
<td>
<button class="btn-red small" @click="viewUser(user)">查看</button>
</td>
</tr>
</tbody>
</table>
<!-- 分页控件 -->
<div class="pagination-container">
<div class="pagination-info">
{{ total }} 条记录 {{ page }}
</div>
<div class="pagination-controls">
<button
class="btn-pagination"
:disabled="page <= 1"
@click="changePage(1)"
>
首页
</button>
<button
class="btn-pagination"
:disabled="page <= 1"
@click="changePage(page - 1)"
>
上一页
</button>
<input
type="number"
class="page-input"
:value="page"
@keyup.enter="jumpToPage"
min="1"
:max="totalPages"
/>
<span class="page-info">/ {{ totalPages }}</span>
<button
class="btn-pagination"
:disabled="page >= totalPages"
@click="changePage(page + 1)"
>
下一页
</button>
<button
class="btn-pagination"
:disabled="page >= totalPages"
@click="changePage(totalPages)"
>
尾页
</button>
</div>
</div>
<!-- 修改后的错题详情弹窗 -->
<div v-if="showWrongQuestionsModal" class="modal-overlay" @click.self="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>错题详情</h3>
</div>
<div class="modal-body">
<div class="wrong-questions-container">
<div v-for="(item, index) in wrongQuestions" :key="index" class="question-item">
<div class="question-stem">
{{ index + 1 }}. {{ item.question.stem }}
</div>
<div class="options">
<div
v-for="(option, optionIndex) in ['A', 'B', 'C', 'D']"
:key="optionIndex"
:class="[{
'option-item': true,
'correct-option': option === item.correctAnswer,
'wrong-option': option === item.userAnswer,
}]"
>
{{ option }}. {{ item.question[option] }}
</div>
</div>
</div>
</div>
<div class="recommendation-section">
<div class="recommendation-title">推荐课程</div>
<div class="recommendation-content">
{{ recommendationText }}
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-red" @click="closeModal">关闭</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getUserStatistics, getWrongQuestions } from '@/api/userStatistics.js'
export default {
name: 'UserStatisticsTable',
props: {
users: {
type: Array,
default: () => []
}
},
data() {
return {
page: 1,
pageSize: 10,
total: 0,
showWrongQuestionsModal: false,
wrongQuestions: [],
currentUserId: null,
recommendationText: '基金操作入门',
currentFilters: {}
}
},
computed: {
totalPages() {
console.log(this.total);
const totalPages = this.total !== 0 ? Math.ceil(this.total / this.pageSize) : 1;
console.log(totalPages);
return totalPages;
}
},
methods: {
async viewUser(user) {
// ID
this.currentUserId = user.id;
// API
try {
await this.loadWrongQuestions(user.jwcode);
//
this.showWrongQuestionsModal = true;
} catch (error) {
console.error('获取用户错题数据失败:', error);
alert('获取用户错题数据失败,请检查网络连接!');
}
},
async loadWrongQuestions(jwcode) {
const params = {
jwcode: jwcode,
page: 1,
page_size: 50
}
const response = await getWrongQuestions(params)
if (response.code === 200) {
//
this.wrongQuestions = response.data.list.map(item => {
//
return {
...item,
question: {
...item.question,
A: item.question.A || item.question.optionA,
B: item.question.B || item.question.optionB,
C: item.question.C || item.question.optionC,
D: item.question.D || item.question.optionD
}
};
});
this.setRecommendationText();
} else {
throw new Error(response.message || '获取错题失败');
}
},
//
setRecommendationText() {
//
const topics = this.wrongQuestions.map(item =>
item.question.stem.toLowerCase()
);
if (topics.some(topic => topic.includes('股票') || topic.includes('股市'))) {
this.recommendationText = '基金操作入门';
} else if (topics.some(topic => topic.includes('金融') || topic.includes('投资'))) {
this.recommendationText = '金融投资基础';
} else {
this.recommendationText = '量能擒牛';
}
},
closeModal() {
this.showWrongQuestionsModal = false;
this.wrongQuestions = [];
this.currentUserId = null;
},
//
async fetchUserStatistics(page = 1, filters = {}) {
try {
this.page=page;
const params = new URLSearchParams();
params.append('page', page);
params.append('page_size', this.pageSize);
//
// if (filters.type=='') {
// params.append('user_identity', '');
// }
//
if (filters.startDate) {
params.append('start_time', filters.startDate);
}
if (filters.endDate) {
params.append('end_time', filters.endDate);
}
if(filters.startDate>filters.endDate){
alert('开始时间不能晚于结束时间');
return;
}
//
if (filters.userName) {
params.append('user_name', filters.userName);
}
//
if (filters.jingwangId) {
params.append('jwcode', filters.jingwangId);
}
//
if (filters.user_identity) {
params.append('user_identity', filters.user_identity);
}
const response = await getUserStatistics(params)
console.log(response);
if (response.code === 200) {
const formattedData = response.data.list||[];
this.page = page;
const totalRaw = response.data.total;
this.total = Number.isFinite(Number(totalRaw)) ? Number(totalRaw) : (formattedData.length > 0 ? formattedData.length : 0);
//
this.$emit('data-loaded', {
list: formattedData,
total: this.total
});
} else {
// console.error(':', response.message)
alert('输入内容错误')
this.total = 0;
this.page = 1;
//
this.$emit('data-loaded', {
list: [],
total: 0
});
}
} catch (error) {
console.error('获取用户数据失败:', error)
this.total = 0;
this.page = 1;
this.$emit('data-loaded', {
list: [],
total: 0
});
}
},
// async Checkbox(jwcode) {
// const params = {
// jwcode: jwcode,
// page: 1,
// page_size: 50
// }
// const response = await axios.post('http://192.168.40.48:8000/api/knowledge/wrong-questions',params)
// this.wprongQuestions = response.data.list;
// },
//
changePage(newPage) {
if (newPage >= 1 && newPage <= this.totalPages) {
this.$emit('page-changed', newPage);
}
},
jumpToPage(event) {
const targetPage = parseInt(event.target.value);
if (targetPage >= 1 && targetPage <= this.totalPages) {
this.$emit('page-changed', targetPage);
} else {
event.target.value = this.page;
}
}
},
onMounted() {
this.fetchQuestions();
this.totalPages();
},
}
</script>
<style scoped>
.table-container {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
table {
width: 100%;
border-collapse: collapse;
background-color: white;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: normal;
color: #333;
}
tr:hover {
background-color: #f9f9f9;
}
/* 分页样式 */
.pagination-container {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 20px;
padding: 0 10px;
}
.pagination-info {
font-size: 14px;
color: #666;
margin-right: 20px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 10px;
}
.btn-pagination {
padding: 6px 12px;
background-color: #f2f2f2;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-pagination:hover:not(:disabled) {
background-color: #e0e0e0;
}
.btn-pagination:disabled {
background-color: #f5f5f5;
color: #ccc;
cursor: not-allowed;
}
.page-input {
width: 50px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-size: 14px;
}
.page-info {
font-size: 14px;
color: #666;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
width: 80%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
.wrong-questions-container {
max-height: 500px;
overflow-y: auto;
margin-bottom: 20px;
}
.question-item {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.question-stem {
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.options {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-item {
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.correct-option {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.wrong-option {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.hidden-option {
display: none;
}
.recommendation-section {
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.recommendation-title {
font-weight: bold;
margin-bottom: 8px;
color: #495057;
}
.recommendation-content {
color: #6c757d;
line-height: 1.5;
}
.modal-footer {
display: flex;
justify-content: flex-end;
padding: 20px;
border-top: 1px solid #eee;
}
.btn-red {
background-color: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.btn-red:hover {
background-color: #c82333;
}
</style>

125
src/components/WrongQuestion/WrongQuestionSearch.vue

@ -0,0 +1,125 @@
<!-- src/components/WrongQuestion/WrongQuestionSearch.vue -->
<template>
<div>
<div class="top">
<h2>错题统计</h2>
</div>
<div class="search-area">
<!-- 题目类型筛选项 -->
<div class="search-item">
<h3>题目类型</h3>
<select v-model="filters.questionType">
<option value="">全部</option>
<option>股票知识</option>
<option>企业文化</option>
</select>
</div>
<!-- 题干关键词搜索项 -->
<div class="search-item">
<h3>题干查找</h3>
<input type="text" v-model="filters.keyword" placeholder="请输入题干关键词" />
</div>
<!-- 推荐课程筛选项 -->
<div class="search-item">
<h3>推荐课程</h3>
<select v-model="filters.course">
<option value="">全部</option>
<option>量能擒牛</option>
<option>价格破译</option>
<option>量价时空综合</option>
</select>
</div>
<!-- 操作按钮组 -->
<div class="btn-group">
<button class="btn-red" @click="searchWrongQuestions">查找</button>
<button class="btn-red" @click="exportExcel">Excel导出</button>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'WrongQuestionSearch',
data() {
return {
filters: {
questionType: '', //
keyword: '', //
course: '' //
},
currentPage: 1, //
total: 0 //
}
},
methods: {
searchWrongQuestions(page = 1) {
this.currentPage = page;
const searchParams = {
...this.filters,
page: page
};
this.$emit('search', searchParams);
},
//
async handlePageChange(page) {
await this.searchWrongQuestions(page);
},
//
resetPage() {
this.currentPage = 1;
},
async exportExcel() {
try{
const Params = {};
const questionTypeIdMap = {
'股票知识': 1,
'企业文化': 2
};
if (this.filters.questionType) {
Params.question_type_id = questionTypeIdMap[this.filters.questionType];
}
const courseRecommendationIdMap = {
'量能擒牛': 1,
'价格破译': 2,
'量价时空综合': 3
};
if (this.filters.course) {
Params.course_recommendation_id = courseRecommendationIdMap[this.filters.course];
}
if (this.filters.keyword) {
Params.stem = this.filters.keyword;
}
const response = await axios.post(
'http://192.168.40.41:8000/admin/questions/export',
Params,
{ responseType: 'blob' }
);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', '错题统计表.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
alert('导出成功!');
}catch (error) {
console.error('导出 Excel 失败:', error);
alert('网络错误,请检查连接!');
}
}
}
}
</script>
<style>
.top{
margin-bottom: 10px;
}
</style>

534
src/components/WrongQuestion/WrongQuestionTable.vue

@ -0,0 +1,534 @@
<!-- src/components/WrongQuestion/WrongQuestionTable.vue -->
<template>
<div class="table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>题干</th>
<th>题目类型</th>
<th>出错次数</th>
<th>出错率</th>
<th>推荐课程</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in wrongQuestions" :key="item.id">
<td>{{ (page- 1) * pageSize + index + 1 }}</td>
<td>{{ item.stem }}</td>
<td>{{ item.questionTypeName }}</td>
<td>{{ item.errorCount }}</td>
<td>{{ item.errorRate }}%</td>
<td>{{ item.CrName }}</td>
<td class="operation-cell">
<button class="btn-red small" @click="viewUser(item)">出错用户</button>
<button class="btn-red small" @click="viewQuestion(item)">查看题目</button>
</td>
</tr>
</tbody>
</table>
<!-- 分页控件 -->
<div class="pagination-container">
<div class="pagination-info">
{{ total }} 条记录 {{ page }}
</div>
<div class="pagination-controls">
<button
class="btn-pagination"
:disabled="page <= 1"
@click="changePage(1)"
>
首页
</button>
<button
class="btn-pagination"
:disabled="page <= 1"
@click="changePage(page - 1)"
>
上一页
</button>
<input
type="number"
class="page-input"
:value="page"
@keyup.enter="jumpToPage"
min="1"
:max="totalPages"
/>
<span class="page-info">/ {{ totalPages }}</span>
<button
class="btn-pagination"
:disabled="page >= totalPages"
@click="changePage(page + 1)"
>
下一页
</button>
<button
class="btn-pagination"
:disabled="page >= totalPages"
@click="changePage(totalPages)"
>
尾页
</button>
</div>
</div>
<!-- 用户弹窗 -->
<div v-if="showUserModal" class="modal-overlay">
<div class="modal-content">
<h3>出错用户列表</h3>
<table class="user-table">
<thead>
<tr>
<th>用户名称</th>
<th>用户身份</th>
<th>出错次数</th>
</tr>
</thead>
<tbody>
<tr v-for="user in errorUsers" :key="user.user_name">
<td>{{ user.user_name }}</td>
<td>{{ user.user_identity }}</td>
<td>{{ user.error_count }}</td>
</tr>
</tbody>
</table>
<button class="btn-close" @click="closeModal">关闭</button>
</div>
</div>
<!-- 查看题目详情弹窗 -->
<div v-if="showViewModal" class="modal-overlay" @click.self="closeViewModal">
<div class="view-modal-content" @click.stop>
<div class="modal-header">
<h3>题目详情</h3>
<button class="close-btn" @click="closeViewModal">×</button>
</div>
<div class="view-modal-body">
<p class="question-text">{{ currentQuestion.stem }}</p>
<div class="options-container">
<div
v-for="option in ['A', 'B', 'C', 'D']"
:key="option"
:class="[option === currentQuestion.correctAnswer ? 'option-item-correct' : 'option-item']"
>
{{ `${option}. ${currentQuestion[`option${option}`]}` }}
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-red" @click="closeViewModal">退出</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getQuestions } from '@/api/question.js';
export default {
name: 'WrongQuestionTable',
data() {
return {
wrongQuestions: [],
total: 0,
page: 1,
pageSize: 20,
showUserModal: false,
errorUsers: [],
showViewModal: false,
currentQuestion: {},
currentFilters: {}
}
},
computed: {
totalPages() {
try {
return this.total !== 0 ? Math.ceil(this.total / this.pageSize) : 1;
} catch(error) {
console.error('计算总页数时出错:', error);
return 1;
}
}
},
methods: {
setFilters(filters) {
this.currentFilters = filters;
this.page = 1;
this.fetchWrongQuestions();
},
async fetchWrongQuestions() {
try {
const params = new URLSearchParams();
params.append('page', this.page);
params.append('page_size', this.pageSize);
//
const questionTypeIdMap = {
'股票知识': 1,
'企业文化': 2
};
if (this.currentFilters.questionType) {
params.append('question_type_id', questionTypeIdMap[this.currentFilters.questionType]);
}
const courseRecommendationIdMap = {
'量能擒牛': 1,
'价格破译': 2,
'量价时空综合': 3
};
if (this.currentFilters.course) {
params.append('course_recommendation_id', courseRecommendationIdMap[this.currentFilters.course]);
}
if (this.currentFilters.keyword) {
params.append('stem', this.currentFilters.keyword);
}
const response = await getQuestions(params);
if (response.data.code === 200) {
this.wrongQuestions = response.data.data.list
this.total = response.data.data.total;
} else {
console.error('接口返回错误:', response.data.msg)
}
} catch (error) {
console.error('获取错题数据失败:', error)
}
},
viewUser(item) {
this.$emit('view-user', item)
this.fetchErrorUsers(item.id)
},
async fetchErrorUsers(questionId) {
try {
const { getUsersByQuestionId } = await import('@/api/wrongQuestion')
const response = await getUsersByQuestionId(questionId)
if (response.data.code === 200) {
this.errorUsers = response.data.data.list || []
this.showUserModal = true
} else {
console.error('获取用户失败:', response.data.msg)
}
} catch (error) {
console.error('请求失败:', error)
}
},
async viewQuestion(item) {
const row = document.querySelector(`[data-id="${item.id}"]`)
if (row) {
row.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
try {
// 使API
const params = {
Page: 1,
PageSize: 100,
id: item.id
}
const response = await getQuestions(params)
if (response.data.code === 200 && response.data.data.list && response.data.data.list.length > 0) {
const rawQuestion = response.data.data.list.find(q => q.id === item.id)
if (!rawQuestion) {
alert('未找到该题目!')
return
}
this.currentQuestion = {
id: rawQuestion.id,
stem: rawQuestion.stem,
optionA: rawQuestion.A,
optionB: rawQuestion.B,
optionC: rawQuestion.C,
optionD: rawQuestion.D,
correctAnswer: rawQuestion.correctAnswer,
questionTypeName: rawQuestion.questionTypeName,
CrName: rawQuestion.CrName
}
this.showViewModal = true
} else {
alert('未找到该题目!')
}
} catch (error) {
console.error('获取题目详情失败:', error)
alert('网络错误,请检查连接!')
}
},
closeViewModal() {
this.showViewModal = false
},
changePage(newPage) {
if (newPage >= 1 && newPage <= this.totalPages) {
this.page = newPage;
this.fetchWrongQuestions();
}
},
jumpToPage(event) {
const targetPage = parseInt(event.target.value)
if (targetPage >= 1 && targetPage <= this.totalPages) {
this.page = targetPage;
this.fetchWrongQuestions();
} else {
event.target.value = this.page
}
},
closeModal() {
this.showUserModal = false
this.errorUsers = []
}
},
mounted() {
this.fetchWrongQuestions();
}
}
</script>
<style scoped>
.table-container {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
table {
width: 100%;
border-collapse: collapse;
background-color: white;
}
th,td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: normal;
color: #333;
display: table-cell !important;
vertical-align: middle !important;
}
tr:hover {
background-color: #f9f9f9;
}
.operation-cell {
display: flex;
gap: 16px;
}
/* 分页样式 */
.pagination-container {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 20px;
padding: 0 10px;
}
.pagination-info {
font-size: 14px;
color: #666;
margin-right: 20px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 10px;
}
.btn-pagination {
padding: 6px 12px;
background-color: #f2f2f2;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-pagination:hover:not(:disabled) {
background-color: #e0e0e0;
}
.btn-pagination:disabled {
background-color: #f5f5f5;
color: #ccc;
cursor: not-allowed;
}
.page-input {
width: 50px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-size: 14px;
}
.page-info {
font-size: 14px;
color: #666;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 600px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.view-modal-content {
background-color: white;
border-radius: 8px;
width: 750px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.view-modal-body {
padding: 20px;
max-height: 600px;
overflow-y: auto;
}
.question-text {
font-size: 16px;
margin-bottom: 20px;
line-height: 1.5;
}
.options-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.option-item {
padding: 10px;
border-radius: 6px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
transition: all 0.2s;
}
.option-item-correct {
padding: 10px;
border-radius: 6px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
transition: all 0.2s;
background-color: #dc3545 !important;
color: white !important;
border-color: #c82333 !important;
font-weight: bold;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 5px;
border-radius: 50%;
transition: color 0.2s;
}
.close-btn:hover {
color: #e74c3c;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 16px;
}
.user-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.user-table th,
.user-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.user-table th {
background-color: #f2f2f2;
}
.btn-close {
margin-top: 15px;
padding: 8px 16px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>

23
src/main.js

@ -0,0 +1,23 @@
// 导入Vue框架
import Vue from 'vue'
// 导入根组件App.vue
import App from './App.vue'
// 导入路由配置
import router from "@/router";
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// 必须要有这一行
Vue.use(ElementUI)
// 关闭生产环境提示
Vue.config.productionTip = false
// 创建Vue实例
new Vue({
// 注入路由配置
router,
// 渲染函数,将App组件渲染到页面
render: h => h(App),
// 挂载到id为app的DOM元素上
}).$mount('#app')

49
src/router/index.js

@ -0,0 +1,49 @@
// src/router/index.js
// 导入Vue框架
import Vue from 'vue'
// 导入Vue Router插件
import VueRouter from 'vue-router'
// 导入主页面组件
// import MainPage from '@/views/MainPage.vue'
// 导入题库管理页面组件
import QuestionManage from '@/views/QuestionManage.vue'
// 导入用户统计数据页面组件
import UserStatistics from '@/views/UserStatistics.vue'
// 导入错题统计页面组件
import WrongQuestion from '@/views/WrongQuestion.vue'
// 在Vue中使用VueRouter插件
Vue.use(VueRouter)
// 定义路由配置数组
const routes = [
// {
// path: '/', // 根路径
// component: MainPage // 使用主页面作为默认页面
// },
{
path: '/', // 题库管理路径
name: 'QuestionManage', // 路由名称
component: QuestionManage // 对应的组件
},
{
path: '/users', // 用户统计数据路径
name: 'UserStatistics', // 路由名称
component: UserStatistics // 对应的组件
},
{
path: '/wrong-questions', // 错题统计路径
name: 'WrongQuestion', // 路由名称
component: WrongQuestion // 对应的组件
}
]
// 创建VueRouter实例
const router = new VueRouter({
mode: 'history', // 使用HTML5 History模式
base: process.env.BASE_URL, // 设置应用的基路径
routes // 路由配置
})
// 导出路由器实例供main.js使用
export default router

71
src/views/MainPage.vue

@ -0,0 +1,71 @@
<!-- src/views/MainPage.vue -->
<template>
<div class="main-page">
<!-- 中央按钮 -->
<div class="center-btn">
<button class="btn-red" @click="goToEvaluationBackend">
测评后台
</button>
</div>
</div>
</template>
<script>
export default {
name: 'MainPage',
methods: {
goToEvaluationBackend() {
//
this.$router.push('/questions')
}
}
}
</script>
<style scoped>
/* 主页面容器样式 */
.main-page {
display: flex; /* 使用弹性布局 */
flex-direction: column; /* 垂直排列子元素 */
align-items: center; /* 水平居中对齐 */
justify-content: center; /* 垂直居中对齐 */
height: 100vh; /* 占满整个视口高度 */
background-color: #f5f5f5; /* 设置背景颜色为浅灰色 */
}
/* 标题样式(虽然模板中未使用,但保留样式定义) */
.title {
font-size: 24px; /* 字体大小 */
color: #333; /* 字体颜色为深灰色 */
margin-bottom: 50px; /* 下边距50像素 */
}
/* 中央按钮容器 */
.center-btn {
text-align: center; /* 文本水平居中 */
}
/* 红色按钮基础样式 */
.btn-red {
padding: 15px 30px; /* 内边距:上下15px,左右30px */
background-color: #e74c3c; /* 背景颜色为红色 */
color: white; /* 文字颜色为白色 */
border: none; /* 无边框 */
border-radius: 8px; /* 圆角边框 */
cursor: pointer; /* 鼠标悬停时显示手型光标 */
font-size: 16px; /* 字体大小 */
box-shadow: 0 4px 6px rgba(0,0,0,0.1); /* 添加阴影效果 */
transition: transform 0.2s; /* 变换动画持续时间 */
}
/* 红色按钮悬停状态样式 */
.btn-red:hover {
background-color: #c0392b; /* 悬停时更深的红色背景 */
transform: translateY(-2px); /* 向上移动2像素 */
}
/* 红色按钮激活(按下)状态样式 */
.btn-red:active {
transform: translateY(0); /* 恢复原位置 */
}
</style>

40
src/views/QuestionManage.vue

@ -0,0 +1,40 @@
<!-- src/views/QuestionManage.vue -->
<template>
<div class="question-management">
<QuestionSearch
ref="questionSearch"
@search-result="handleSearchResult"
@question-added="refreshQuestions"
/>
<QuestionTable ref="questionTable" @page-changed="handlePageChanged" />
</div>
</template>
<script>
import QuestionSearch from '@/components/Question/QuestionSearch.vue'
import QuestionTable from '@/components/Question/QuestionTable.vue'
export default {
name: 'QuestionManage',
components: {
QuestionSearch,
QuestionTable
},
methods: {
handleSearchResult(data) {
this.$refs.questionTable.setData(data)
},
refreshQuestions() {
this.$refs.questionTable.fetchQuestions()
},
//
async handlePageChanged(page) {
//
await this.$refs.questionSearch.handlePageChange(page);
}
}
}
</script>

72
src/views/UserStatistics.vue

@ -0,0 +1,72 @@
<!-- UserStatistics.vue -->
<template>
<div class="user-statistics">
<!-- 用户统计数据搜索组件 -->
<UserStatisticsSearch @search="handleSearch" />
<!-- 用户统计数据表格组件 -->
<UserStatisticsTable
ref="userStatisticsTable"
:users="userStatistics"
@page-changed="handlePageChanged"
@data-loaded="handleDataLoaded"
/>
</div>
</template>
<script>
import UserStatisticsSearch from '@/components/UserStatistics/UserStatisticsSearch.vue'
import UserStatisticsTable from '@/components/UserStatistics/UserStatisticsTable.vue'
export default {
name: 'UserStatistics',
components: {
UserStatisticsSearch,
UserStatisticsTable
},
data() {
return {
userStatistics: [],
filters: {
type: '',
startDate: '',
endDate: '',
userName: '',
jingwangId: '',
user_identity: ''
},
page: 1,
pageSize: 10,
total: 0
}
},
methods: {
//
handleSearch(filters) {
this.filters = { ...filters }; //
this.page = 1;
this.$refs.userStatisticsTable.fetchUserStatistics(1, filters);
},
//
handlePageChanged(page) {
this.page = page;
this.$refs.userStatisticsTable.fetchUserStatistics(page, this.filters);
},
//
handleDataLoaded(data) {
this.userStatistics = data.list;
this.total = data.total;
}
},
async mounted() {
//
await this.$refs.userStatisticsTable.fetchUserStatistics(1, this.filters);
}
}
</script>
<style scoped>
.user-statistics {
padding: 20px;
}
</style>

49
src/views/WrongQuestion.vue

@ -0,0 +1,49 @@
<!-- src/views/WrongQuestion.vue -->
<template>
<div class="wrong-question">
<!-- 搜索区域组件 -->
<WrongQuestionSearch
ref="wrongQuestionSearch"
@search="handleSearch"
/>
<!-- 错题表格组件 -->
<WrongQuestionTable
ref="wrongQuestionTable"
@view-user="viewUser"
@view-question="viewQuestion"
/>
</div>
</template>
<script>
import WrongQuestionSearch from '@/components/WrongQuestion/WrongQuestionSearch.vue'
import WrongQuestionTable from '@/components/WrongQuestion/WrongQuestionTable.vue'
export default {
name: 'WrongQuestion',
components: {
WrongQuestionSearch,
WrongQuestionTable
},
methods: {
handleSearch(filters) {
//
this.$refs.wrongQuestionTable.setFilters(filters);
},
viewUser(item) {
console.log('查看出错用户:', item)
},
viewQuestion(item) {
console.log('查看题目:', item)
}
}
}
</script>
<style scoped>
.wrong-question {
padding: 20px;
}
</style>

4
vue.config.js

@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
Loading…
Cancel
Save