Compare commits
merge into: majun:master
majun:backup-before-author-change
majun:chenzhen/feature-20251107115823-股票知识测评
majun:daijiajun/feature-20251107115823-股票知识测评
majun:main
majun:master
majun:milestone-20251107-股票知识测评
pull from: majun:daijiajun/feature-20251107115823-股票知识测评
majun:backup-before-author-change
majun:chenzhen/feature-20251107115823-股票知识测评
majun:daijiajun/feature-20251107115823-股票知识测评
majun:main
majun:master
majun:milestone-20251107-股票知识测评
No commits in common. 'master' and 'daijiajun/feature-20251107115823-股票知识测评' have entirely different histories.
master
...
daijiajun/
30 changed files with 23249 additions and 9 deletions
-
28.gitignore
-
24README.md
-
5babel.config.js
-
19jsconfig.json
-
19804package-lock.json
-
46package.json
-
BINpublic/favicon.ico
-
17public/index.html
-
155src/App.vue
-
18src/api/question.js
-
32src/api/userStatistics.js
-
19src/api/wrongQuestion.js
-
BINsrc/assets/logo.png
-
BINsrc/assets/screenshot.jpg
-
36src/components/Layout/Header.vue
-
60src/components/Layout/Sidebar.vue
-
465src/components/Question/QuestionSearch.vue
-
747src/components/Question/QuestionTable.vue
-
38src/components/Tabs/TabNavigation.vue
-
221src/components/UserStatistics/UserStatisticsSearch.vue
-
557src/components/UserStatistics/UserStatisticsTable.vue
-
125src/components/WrongQuestion/WrongQuestionSearch.vue
-
534src/components/WrongQuestion/WrongQuestionTable.vue
-
23src/main.js
-
49src/router/index.js
-
71src/views/MainPage.vue
-
40src/views/QuestionManage.vue
-
72src/views/UserStatistics.vue
-
49src/views/WrongQuestion.vue
-
4vue.config.js
@ -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? |
||||
@ -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/). |
||||
@ -0,0 +1,5 @@ |
|||||
|
module.exports = { |
||||
|
presets: [ |
||||
|
'@vue/cli-plugin-babel/preset' |
||||
|
] |
||||
|
} |
||||
@ -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
File diff suppressed because it is too large
View File
@ -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" |
||||
|
] |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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) // 新增删除题目的接口
|
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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' |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
After Width: 200 | Height: 200 | Size: 6.7 KiB |
|
After Width: 1361 | Height: 854 | Size: 243 KiB |
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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') |
||||
@ -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 |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,4 @@ |
|||||
|
const { defineConfig } = require('@vue/cli-service') |
||||
|
module.exports = defineConfig({ |
||||
|
transpileDependencies: true |
||||
|
}) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue