Browse Source

total

daijiajun/feature-20251107115823-股票知识测评
daijiajun 2 months ago
parent
commit
0729565a02
  1. 10
      src/App.vue
  2. 18
      src/api/question.js
  3. 32
      src/api/userStatistics.js
  4. 19
      src/api/wrongQuestion.js
  5. BIN
      src/assets/screenshot.jpg
  6. 406
      src/components/Question/QuestionSearch.vue
  7. 883
      src/components/Question/QuestionTable.vue
  8. 119
      src/components/UserStatistics/UserStatisticsSearch.vue
  9. 475
      src/components/UserStatistics/UserStatisticsTable.vue
  10. 47
      src/components/WrongQuestion/WrongQuestionSearch.vue
  11. 516
      src/components/WrongQuestion/WrongQuestionTable.vue
  12. 5
      src/router/index.js
  13. 71
      src/views/MainPage.vue
  14. 32
      src/views/QuestionManage.vue
  15. 76
      src/views/UserStatistics.vue
  16. 73
      src/views/WrongQuestion.vue
  17. 2
      vue.config.js

10
src/App.vue

@ -139,13 +139,15 @@ body {
opacity: 0.5;
}
/* 底部按钮定位样式 */
/* 修改后的底部按钮定位样式 */
.footer-btn {
position: absolute;
bottom: 20px;
right: 20px;
position: fixed; /* 改为 fixed 定位 */
bottom: 20px; /* 距离底部 20px */
right: 20px; /* 距离右侧 20px */
z-index: 1000; /* 确保在其他元素之上 */
}
/* 搜索区域样式 */
.search-area {
display: flex;

18
src/api/question.js

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

32
src/api/userStatistics.js

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

19
src/api/wrongQuestion.js

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

BIN
src/assets/screenshot.jpg

After

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

406
src/components/Question/QuestionSearch.vue

@ -1,45 +1,387 @@
<!--搜索区域-->
<!-- src/components/Question/QuestionSearch.vue -->
<template>
<!-- 搜索区域容器 -->
<div class="search-area">
<!-- 题目类型筛选项 -->
<div class="search-item">
<label>题目类型</label>
<select>
<option>股票知识</option>
<option>企业文化</option>
<option>全部</option>
</select>
</div>
<div class="question-search-container">
<!-- 搜索区域容器 -->
<div class="search-area">
<!-- 题目类型筛选项 -->
<div class="search-item">
<label>题目类型</label>
<select v-model="searchForm.questionType">
<option value="">全部</option>
<option>股票知识</option>
<option>企业文化</option>
</select>
</div>
<!-- 题干关键词搜索项 -->
<div class="search-item">
<label>题干查找</label>
<input type="text" placeholder="请输入题干关键词" />
</div>
<!-- 题干关键词搜索项 -->
<div class="search-item">
<label>题干查找</label>
<input type="text" placeholder="请输入题干关键词" v-model="searchForm.keyword" />
</div>
<!-- 课程推荐筛选项 -->
<div class="search-item">
<label>推荐系列</label>
<select>
<option>量能擒牛</option>
<option>价格破译</option>
<!-- 课程推荐筛选项 -->
<div class="search-item">
<label>推荐系列</label>
<select v-model="searchForm.course">
<option value="">全部</option>
<option>量能擒牛</option>
<option>价格破译</option>
<option>量价时空综合</option>
</select>
</div>
</select>
<!-- 操作按钮组 -->
<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 class="btn-group">
<button class="btn-red">查找</button>
<button class="btn-red">新增题目</button>
<button class="btn-red">Excel导出</button>
<!-- 新增题目弹窗 -->
<div v-if="showAddModal" class="modal-overlay" @click.self="closeModal">
<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>
<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';
export default {
//
name: 'QuestionSearch'
}
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 || 0
};
this.$emit('search-result', resultData);
this.total = response.data.data.total || 0;
} 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) {
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 :
this.newQuestion.recommendedCourse === '量价时空综合' ? 3 : '');
//
const response = await axios.post('/admin/questions/update', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (response.data.code === 200) {
this.closeModal();
this.$emit('question-added');
alert('添加题目成功!');
} else {
alert('添加题目失败:' + response.data.msg);
}
} catch (error) {
console.error('添加题目失败:', error);
alert('网络错误,请检查连接!');
}
},
//
async handlePageChange(page) {
await this.handleSearch(page);
},
exportExcel() {
console.log('执行Excel导出');
},
closeModal() {
this.showAddModal = false;
this.resetForm();
},
resetForm() {
this.newQuestion = {
id: 0,
stem: '',
optionA: '',
optionB: '',
optionC: '',
optionD: '',
correctAnswer: 'A',
questionTypeName: '股票知识',
recommendedCourse: ''
};
}
}
};
</script>
<style scoped>
/* 弹窗样式 */
.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>

883
src/components/Question/QuestionTable.vue

@ -1,122 +1,310 @@
<!-- src/components/Question/QuestionTable.vue -->
<template>
<!-- 表格容器 -->
<div class="table-container">
<!-- 题目数据表格 -->
<table>
<!-- 表头部分 -->
<thead>
<tr>
<th>ID</th>
<th>题干</th>
<th>题目类型</th>
<th @click="sort('errorCount')" class="sortable">
<div class="sort-header">
出错次数
<span v-if="sortField === 'errorCount'" class="sort-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
<div class="question-table-container">
<!-- 表格容器 -->
<div class="table-container">
<!-- 题目数据表格 -->
<table>
<!-- 表头部分 -->
<thead>
<tr>
<!-- ID列支持排序 -->
<th @click="sort('id')" class="sortable">
<div class="sort-header">
ID
<!-- 排序图标当按照ID排序时显示 -->
<span v-if="sortField === 'id'" class="sort-icon">
<svg :style="{ transform: sortDirection === 'desc' ? 'rotate(180deg)' : 'rotate(0deg)' }"
width="12" height="12" viewBox="0 0 24 24" fill="none">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
</div>
</th>
<!-- 题干列 -->
<th>题干</th>
<!-- 题目类型列 -->
<th>题目类型</th>
<!-- 出错次数列支持排序 -->
<th @click="sort('errorCount')" class="sortable">
<div class="sort-header">
出错次数
<!-- 排序图标当按照出错次数排序时显示 -->
<span v-if="sortField === 'errorCount'" class="sort-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
</div>
</th>
<!-- 出错率列支持排序 -->
<th @click="sort('errorRate')" class="sortable">
<div class="sort-header">
出错率
<!-- 排序图标当按照出错率排序时显示 -->
<span v-if="sortField === 'errorRate'" class="sort-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
</div>
</th>
<!-- 推荐系列列 -->
<th>推荐系列</th>
<!-- 操作列 -->
<th>操作</th>
</tr>
</thead>
<!-- 表格主体部分 -->
<tbody>
<!-- 数据行遍历sortedItems数组渲染每一条题目数据 -->
<tr v-for="item in sortedItems" :key="item.id" :data-id="item.id">
<td>{{ item.id }}</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">
<!-- 遍历ABCD四个选项 -->
<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 v-if="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>
</th>
<th @click="sort('errorRate')" class="sortable">
<div class="sort-header">
出错率
<span v-if="sortField === 'errorRate'" class="sort-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
<div class="form-row">
<label>推荐课程</label>
<select v-model="editingQuestion.recommendedCourse">
<option value="">请选择</option>
<option value="量能擒牛">量能擒牛</option>
<option value="价格破译">价格破译</option>
<option value="量价时空综合">量价时空综合</option>
</select>
</div>
</th>
<th>推荐课程</th>
<th>操作</th>
</tr>
</thead>
<!-- 表格主体部分 -->
<tbody>
<!-- 示例数据行 -->
<tr v-for="item in sortedItems" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.questionText }}</td>
<td>{{ item.type }}</td>
<td>{{ item.errorCount }}</td>
<td>{{ item.errorRate }}</td>
<td>{{ item.recommendedCourse }}</td>
<td>
<button class="btn-red small">查看</button>
<button class="btn-red small">修改</button>
<button class="btn-red small">删除</button>
</td>
</tr>
</tbody>
</table>
</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 {
sortField: '', //
sortDirection: 'asc', // asc desc
items: [
{
id: 1,
questionText: '以下哪项不是股票的基本特征?',
type: '股票知识',
errorCount: 50,
errorRate: '50%',
recommendedCourse: '量能擒牛'
},
{
id: 2,
questionText: '基金的风险主要来源于?',
type: '基金知识',
errorCount: 30,
errorRate: '30%',
recommendedCourse: '基本面分析'
}
],
sortedItems: [] //
sortField: '', //
sortDirection: 'asc', // (/)
items: [], //
sortedItems: [], //
showViewModal: false, //
currentQuestion: {}, //
page: 1, //
pageSize: 20, //
total: 0 , //
showEditModal: false, //
editingQuestion: {}, //
showDeleteModal: false, //
deleteId: null, // ID
}
},
computed: {
//
totalPages() {
return Math.ceil(this.total / this.pageSize);
}
},
// computed: {
// sortedItems() {
// if (!this.sortField) {
// // ID
// return this.items.sort((a, b) => a.id - b.id)
// }
//
// return this.items.sort((a, b) => {
// const aValue = a[this.sortField]
// const bValue = b[this.sortField]
//
// //
// if (typeof aValue === 'number' && typeof bValue === 'number') {
// return this.sortDirection === 'asc' ? aValue - bValue : bValue - aValue
// } else {
// const strA = String(aValue).toLowerCase()
// const strB = String(bValue).toLowerCase()
// return this.sortDirection === 'asc'
// ? strA.localeCompare(strB)
// : strB.localeCompare(strA)
// }
// })
// }
// },
async mounted() {
//
await this.fetchQuestions()
},
watch: {
//
sortField: {
handler() {
this.updateSortedItems()
},
immediate: true //
immediate: true
},
//
sortDirection: {
handler() {
this.updateSortedItems()
@ -126,19 +314,55 @@ export default {
},
methods: {
//
// setData
setData(data) {
this.items = data.list || data;
this.total = data.total || (data.list ? data.list.length : data.length);
this.updateSortedItems();
},
//
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
this.updateSortedItems()
}
} catch (error) {
console.error('获取题目数据失败:', error)
}
},
//
updateSortedItems() {
// ID
if (!this.sortField) {
this.sortedItems = [...this.items].sort((a, b) => a.id - b.id)
return
}
//
const sorted = [...this.items].sort((a, b) => {
const aValue = a[this.sortField]
const bValue = b[this.sortField]
//
if (typeof aValue === 'number' && typeof bValue === 'number') {
return this.sortDirection === 'asc' ? aValue - bValue : bValue - aValue
} else {
//
const strA = String(aValue).toLowerCase()
const strB = String(bValue).toLowerCase()
return this.sortDirection === 'asc'
@ -149,22 +373,205 @@ export default {
this.sortedItems = sorted
},
//
sort(field) {
if (this.sortField === field) {
//
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'
} else {
//
this.sortField = field
this.sortDirection = 'asc'
}
},
//
// changePage
changePage(newPage) {
if (newPage >= 1 && newPage <= this.totalPages) {
this.page = newPage;
//
this.$emit('page-changed', newPage);
}
},
//
// jumpToPage
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('/admin/questions/update', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (response.data.code === 200) {
this.closeEditModal();
//
await this.fetchQuestions();
alert('修改题目成功!');
} 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()
alert('删除成功!')
} 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;
@ -176,6 +583,55 @@ th {
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;
}
}
/* sort-header 样式 */
.sort-header {
display: flex;
@ -192,6 +648,7 @@ th {
stroke-width: 2;
transition: transform 0.2s;
}
/* 表格容器样式 */
.table-container {
width: 100%;
@ -250,6 +707,226 @@ 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; /* 从500px改为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;
}
.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;
}
</style>

119
src/components/UserStatistics/UserStatisticsSearch.vue

@ -1,3 +1,4 @@
<!-- UserStatisticsSearch.vue -->
<template>
<div class="search-area">
<!-- 题目类型 -->
@ -6,17 +7,21 @@
<select v-model="filters.type">
<option value="">全部</option>
<option value="股票知识">股票知识</option>
<option value="基金知识">基金知识</option>
<option value="投资策略">投资策略</option>
</select>
</div>
<!-- 时间选择 -->
<div class="search-item">
<div class="search-item date-range">
<label>时间选择</label>
<input type="date" v-model="filters.startDate" />
<span style="margin: 0 10px;"></span>
<input type="date" v-model="filters.endDate" />
<div class="date-input-wrapper">
<div class="date-box">
<input type="date" v-model="filters.startDate" />
</div>
<span class="separator"></span>
<div class="date-box">
<input type="date" v-model="filters.endDate" />
</div>
</div>
</div>
<!-- 用户名称 -->
@ -66,10 +71,9 @@ export default {
}
},
methods: {
async searchUserStatistics() {
//
console.log('搜索条件:', this.filters)
// API
searchUserStatistics() {
//
this.$emit('search', {...this.filters});
},
exportToExcel() {
// Excel
@ -78,3 +82,98 @@ export default {
}
}
</script>
<style scoped>
.search-area {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
background-color: white;
border-radius: 4px;
}
.search-item {
display: flex;
flex-direction: column;
min-width: 200px;
}
.search-item label {
margin-bottom: 5px;
font-weight: bold;
}
.search-item input,
.search-item select {
padding: 8px;
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%;
/* 上边距8px,与标签保持间距 */
margin-top: 6px;
}
.date-box {
/* 占据剩余空间 */
flex: 1;
/* 右侧间隔10px */
margin-right: 10px;
}
.date-box:last-child {
/* 最后一个日期框不添加右侧间隔 */
margin-right: 0;
}
.date-box input[type="date"] {
/* 输入框宽度占满容器 */
width: 100%;
/* 输入框高度为34px */
height: 34px;
}
.separator {
/* 左右间隔10px */
margin: 0 10px;
/* 防止文字换行 */
white-space: nowrap;
}
</style>

475
src/components/UserStatistics/UserStatisticsTable.vue

@ -1,37 +1,132 @@
<!-- 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>
<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 in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.userName }}</td>
<td>{{ user.role }}</td>
<td>{{ user.jingwangId }}</td>
<td>{{ user.questionType }}</td>
<td>{{ user.score }}</td>
<td>{{ user.submitTime }}</td>
<td>
<button class="btn-red small" @click="viewUser(user)">查看</button>
</td>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.userName }}</td>
<td>{{ user.user_identity }}</td>
<td>{{ user.jwcode }}</td>
<td>{{ user.questionTypeName }}</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" v-if="total > pageSize">
<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>
<button class="close-btn" @click="closeModal">×</button>
</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">
{{ 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,
'hidden-option': option !== item.correctAnswer && 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: {
@ -40,10 +135,145 @@ export default {
default: () => []
}
},
data() {
return {
page: 1,
pageSize: 10,
total: 0,
showWrongQuestionsModal: false,
wrongQuestions: [],
currentUserId: null,
recommendationText: '基金操作入门'
}
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize);
}
},
methods: {
viewUser(user) {
//
console.log('查看用户:', user)
//
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 {
const params = {
page: page,
page_size: this.pageSize,
...filters
}
const response = await getUserStatistics(params)
if (response.code === 200) {
const formattedData = response.data.list.map(item => ({
id: item.id,
userName: item.user_name,
user_identity: item.user_identity,
jwcode: item.jwcode,
questionTypeName: item.question_type_name || '股票知识',
score: item.score,
createdAt: item.createdAt
}))
this.page = page;
this.total = response.data.total || response.data.list.length;
//
this.$emit('data-loaded', {
list: formattedData,
total: this.total
});
} else {
console.error('接口返回错误:', response.message)
}
} catch (error) {
console.error('获取用户数据失败:', error)
}
},
//
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;
}
}
}
}
@ -78,4 +308,199 @@ th {
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>

47
src/components/WrongQuestion/WrongQuestionSearch.vue

@ -1,15 +1,13 @@
<!-- src/components/WrongQuestion/WrongQuestionSearch.vue -->
<template>
<!-- 搜索区域容器 -->
<div class="search-area">
<!-- 题目类型筛选项 -->
<div class="search-item">
<label>题目类型</label>
<select v-model="filters.type">
<option value="">全部</option>
<option value="股票知识">股票知识</option>
<option value="基金知识">基金知识</option>
<option value="投资策略">投资策略</option>
<option value="1">股票知识</option>
<option value="2">企业文化</option>
</select>
</div>
@ -24,17 +22,15 @@
<label>推荐课程</label>
<select v-model="filters.course">
<option value="">全部</option>
<option value="量能擒牛">量能擒牛</option>
<option value="趋势交易">趋势交易</option>
<option value="基本面分析">基本面分析</option>
<option value="1">量能擒牛</option>
<option value="2">价格破译</option>
<option value="3">量价时空综合</option>
</select>
</div>
<!-- 操作按钮组 -->
<div class="btn-group">
<!-- 查找按钮触发搜索逻辑 -->
<button class="btn-red" @click="searchWrongQuestions">查找</button>
<!-- Excel导出按钮触发导出逻辑 -->
<button class="btn-red" @click="exportToExcel">Excel导出</button>
</div>
</div>
@ -42,27 +38,40 @@
<script>
export default {
//
name: 'WrongQuestionSearch',
data() {
return {
//
filters: {
type: '', //
type: '', // ID
questionText: '', //
course: '' //
}
course: '' // ID
},
currentPage: 1, //
total: 0 //
}
},
methods: {
//
searchWrongQuestions() {
this.$emit('search', this.filters)
searchWrongQuestions(page = 1) {
this.currentPage = page;
const searchParams = {
...this.filters,
page: page
};
this.$emit('search', searchParams);
},
// Excel
//
async handlePageChange(page) {
await this.searchWrongQuestions(page);
},
exportToExcel() {
console.log('导出 Excel')
// 使 `xlsx`
},
//
resetPage() {
this.currentPage = 1;
}
}
}

516
src/components/WrongQuestion/WrongQuestionTable.vue

@ -1,49 +1,58 @@
<!-- src/components/WrongQuestion/WrongQuestionTable.vue -->
<template>
<!-- 表格容器用于展示错题统计数据 -->
<div class="table-container">
<!-- 数据表格 -->
<table>
<!-- 表头部分 -->
<thead>
<tr>
<th>ID</th>
<th @click="sort('id')" class="sortable">
<div class="sort-header">
ID
<span v-if="sortField === 'id'" class="sort-icon">
<svg :style="{ transform: sortDirection === 'desc' ? 'rotate(180deg)' : 'rotate(0deg)' }"
width="12" height="12" viewBox="0 0 24 24" fill="none">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
</div>
</th>
<th>题干</th>
<th>题目类型</th>
<th @click="sort('errorCount')" class="sortable">
<div class="sort-header">
出错次数
<span v-if="sortField === 'errorCount'" class="sort-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
</div>
</th>
<th @click="sort('errorRate')" class="sortable">
<div class="sort-header">
出错率
<span v-if="sortField === 'errorRate'" class="sort-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10L12 15L17 10" stroke="#e74c3c" stroke-width="2" />
<path d="M12 15V3" stroke="#e74c3c" stroke-width="2" />
</svg>
</span>
</div>
</th>
<th>推荐课程</th>
<th>操作</th>
</tr>
</thead>
<!-- 表格主体循环渲染每条错题数据 -->
<tbody>
<tr v-for="item in sortedWrongQuestions" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.questionText }}</td>
<td>{{ item.type }}</td>
<td>{{ item.stem }}</td>
<td>{{ item.questionTypeName }}</td>
<td>{{ item.errorCount }}</td>
<td>{{ item.errorRate }}</td>
<td>{{ item.recommendedCourse }}</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>
@ -51,28 +60,132 @@
</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',
props: {
wrongQuestions: {
type: Array,
default: () => []
}
},
data() {
return {
sortField: '', //
sortDirection: 'asc' // asc desc
wrongQuestions: [],
total: 0,
page: 1,
pageSize: 20,
sortField: '',
sortDirection: 'asc',
showUserModal: false,
errorUsers: [],
showViewModal: false,
currentQuestion: {},
currentFilters: {}
}
},
computed: {
sortedWrongQuestions() {
if (!this.sortField) {
// ID
return [...this.wrongQuestions].sort((a, b) => a.id - b.id)
}
@ -80,7 +193,12 @@ export default {
const aValue = a[this.sortField]
const bValue = b[this.sortField]
//
if (this.sortField === 'errorRate') {
const numA = parseFloat(String(aValue).replace('%', '')) || 0
const numB = parseFloat(String(bValue).replace('%', '')) || 0
return this.sortDirection === 'asc' ? numA - numB : numB - numA
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
return this.sortDirection === 'asc' ? aValue - bValue : bValue - aValue
} else {
@ -91,15 +209,123 @@ export default {
: strB.localeCompare(strA)
}
})
},
totalPages() {
return Math.ceil(this.total / this.pageSize)
}
},
methods: {
setFilters(filters) {
this.currentFilters = filters;
this.page = 1;
this.fetchWrongQuestions();
},
async fetchWrongQuestions() {
try {
const params = {
Page: this.page,
PageSize: this.pageSize,
question_type_id: this.currentFilters.type ? parseInt(this.currentFilters.type) : undefined,
course_recommendation_id: this.currentFilters.course ? parseInt(this.currentFilters.course) : undefined,
stem: this.currentFilters.questionText || '',
sort_field: this.sortField,
sort_direction: this.sortDirection
}
// 使 getQuestions axios.post
const response = await getQuestions(params)
if (response.data.code === 200) {
this.wrongQuestions = response.data.data.list.map(item => ({
id: item.id,
stem: item.stem,
questionTypeName: item.questionTypeName,
errorCount: item.errorCount,
errorRate: `${item.errorRate}%`,
CrName: item.CrName
}))
this.total = response.data.data.total || 0;
} else {
console.error('接口返回错误:', response.data.msg)
}
} catch (error) {
console.error('获取错题数据失败:', error)
}
}
,
viewUser(item) {
console.log('查看出错用户:', item)
this.$emit('view-user', item)
this.fetchErrorUsers(item.id)
},
viewQuestion(item) {
console.log('查看题目:', item)
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
},
sort(field) {
if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'
@ -107,7 +333,33 @@ export default {
this.sortField = field
this.sortDirection = 'asc'
}
this.fetchWrongQuestions();
},
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>
@ -136,8 +388,8 @@ th {
background-color: #f2f2f2;
font-weight: normal;
color: #333;
cursor: pointer; /* 添加鼠标指针 */
user-select: none; /* 防止文字选中 */
cursor: pointer;
user-select: none;
display: table-cell !important;
vertical-align: middle !important;
}
@ -146,27 +398,23 @@ tr:hover {
background-color: #f9f9f9;
}
/* 操作列按钮间距调整 */
.operation-cell {
display: flex;
gap: 16px; /* 更宽的间距 */
gap: 16px;
}
/* sort-header 样式 */
.sort-header {
display: flex;
align-items: center;
gap: 6px;
}
/* 可排序列样式 */
.sortable {
display: flex;
align-items: center;
gap: 6px;
}
/* 排序图标样式 */
.sort-icon svg {
width: 12px;
height: 12px;
@ -176,8 +424,198 @@ tr:hover {
transition: transform 0.2s;
}
/* 悬 hover 效果 */
th:hover {
background-color: #e0e0e0;
}
/* 分页样式 */
.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>

5
src/router/index.js

@ -1,7 +1,10 @@
// 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'
// 导入用户统计数据页面组件
@ -16,7 +19,7 @@ Vue.use(VueRouter)
const routes = [
{
path: '/', // 根路径
redirect: '/questions' // 重定向到题目管理页面
component: MainPage // 使用主页面作为默认页面
},
{
path: '/questions', // 题库管理路径

71
src/views/MainPage.vue

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

32
src/views/QuestionManage.vue

@ -1,26 +1,40 @@
<!-- src/views/QuestionManage.vue -->
<template>
<!-- 题库管理页面容器 -->
<div class="question-management">
<!-- 题目搜索组件 -->
<QuestionSearch />
<!-- 题目表格组件 -->
<QuestionTable />
<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>

76
src/views/UserStatistics.vue

@ -1,62 +1,76 @@
<!-- UserStatistics.vue -->
<template>
<!-- 用户统计数据页面容器 -->
<div class="user-statistics">
<!-- 用户统计数据搜索组件 -->
<UserStatisticsSearch />
<!-- 用户统计数据表格组件传递用户数据 -->
<UserStatisticsTable :users="userStatistics" />
<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: [
{
id: 1, // ID
userName: '张三', //
role: '学员', //
jingwangId: '90048184', // ID
questionType: '股票知识', //
score: 50, //
submitTime: '2025-11-04 10:00:00' //
}
]
userStatistics: [],
filters: {
type: '',
startDate: '',
endDate: '',
userName: '',
jingwangId: '',
role: ''
},
page: 1,
pageSize: 10,
total: 0
}
},
//
methods: {
//
fetchUserStatistics() {
//
//
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;
}
},
//
mounted() {
//
this.fetchUserStatistics()
async mounted() {
//
await this.$refs.userStatisticsTable.fetchUserStatistics(1, this.filters);
}
}
</script>
<style scoped>
/* 用户统计数据页面样式 */
.user-statistics {
padding: 20px; /* 内边距20px */
padding: 20px;
}
</style>

73
src/views/WrongQuestion.vue

@ -1,78 +1,49 @@
<!-- src/views/WrongQuestion.vue -->
<template>
<!-- 错题统计页面容器 -->
<div class="wrong-question">
<!-- 搜索区域组件用于筛选错题数据 -->
<WrongQuestionSearch @search="handleSearch" />
<!-- 错题表格组件展示筛选后的错题列表 -->
<WrongQuestionTable :wrongQuestions="wrongQuestions" />
<!-- 搜索区域组件 -->
<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'
import axios from "axios";
export default {
//
name: 'WrongQuestion',
//
components: {
WrongQuestionSearch,
WrongQuestionTable
},
//
data() {
return {
//
wrongQuestions: [
{
id: 1,
questionText: '以下哪项不是股票的基本特征?',
type: '股票知识',
errorCount: 50,
errorRate: '50%',
recommendedCourse: '量能擒牛'
},
{
id: 2,
questionText: '基金的风险主要来源于?',
type: '基金知识',
errorCount: 30,
errorRate: '30%',
recommendedCourse: '基本面分析'
}
]
}
},
//
methods: {
handleSearch(filters) {
this.fetchWrongQuestions(filters)
//
this.$refs.wrongQuestionTable.setFilters(filters);
},
viewUser(item) {
console.log('查看出错用户:', item)
},
async fetchWrongQuestions(filters = {}) {
try {
const response = await axios.get('/api/wrong-questions', { params: filters })
this.wrongQuestions = response.data
} catch (error) {
console.error('搜索失败:', error)
}
viewQuestion(item) {
console.log('查看题目:', item)
}
}
,
//
mounted() {
//
this.fetchWrongQuestions()
}
}
</script>
<style scoped>
/* 错题统计页面整体样式 */
.wrong-question {
padding: 20px; /* 内边距设置为20px,保持内容与边界间距 */
padding: 20px;
}
</style>

2
vue.config.js

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