guoyu/Study-Vue-redis/ry-study-ui/src/views/study/voiceEvaluation/index.vue
xiao12feng8 015d10b3b5 清理
2026-01-30 18:23:58 +08:00

671 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="学员姓名" prop="studentName">
<el-input
v-model="queryParams.studentName"
placeholder="请输入学员姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="课程名称" prop="courseName">
<el-input
v-model="queryParams.courseName"
placeholder="请输入课程名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="评测时间">
<el-date-picker
v-model="daterangeEvaluationTime"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAddContent"
v-hasPermi="['study:voiceEvaluationContent:add']"
>增加内容</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
@click="handleEditContent"
v-hasPermi="['study:voiceEvaluationContent:edit']"
>编辑内容</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['study:voiceEvaluation:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['study:voiceEvaluation:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="voiceEvaluationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="评测ID" align="center" prop="id" />
<el-table-column label="学员姓名" align="center" prop="studentName" />
<el-table-column label="课程名称" align="center" prop="courseName" />
<el-table-column label="评测内容" align="center" prop="content" :show-overflow-tooltip="true" width="200">
<template slot-scope="scope">
<span>{{ scope.row.content ? (scope.row.content.length > 30 ? scope.row.content.substring(0, 30) + '...' : scope.row.content) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="总分" align="center" prop="score">
<template slot-scope="scope">
<span style="color: #409eff; font-weight: bold;">{{ scope.row.score || 0 }}分</span>
</template>
</el-table-column>
<el-table-column label="准确度" align="center" prop="accuracy">
<template slot-scope="scope">
<span>{{ scope.row.accuracy || 0 }}分</span>
</template>
</el-table-column>
<el-table-column label="流畅度" align="center" prop="fluency">
<template slot-scope="scope">
<span>{{ scope.row.fluency || 0 }}分</span>
</template>
</el-table-column>
<el-table-column label="完整度" align="center" prop="completeness">
<template slot-scope="scope">
<span>{{ scope.row.completeness || 0 }}分</span>
</template>
</el-table-column>
<el-table-column label="发音" align="center" prop="pronunciation">
<template slot-scope="scope">
<span>{{ scope.row.pronunciation || 0 }}分</span>
</template>
</el-table-column>
<el-table-column label="评测时间" align="center" prop="evaluationTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.evaluationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="提交状态" align="center" prop="isSubmitted" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.isSubmitted === 1" type="success" size="small">已提交</el-tag>
<el-tag v-else type="info" size="small">未提交</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" align="center" prop="submitTime" width="180">
<template slot-scope="scope">
<span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['study:voiceEvaluation:query']"
>查看</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['study:voiceEvaluation:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 内容管理对话框 -->
<el-dialog :title="contentForm.id ? '编辑内容' : '增加内容'" :visible.sync="contentOpen" width="800px" append-to-body>
<el-form ref="contentForm" :model="contentForm" :rules="contentRules" label-width="100px">
<el-form-item label="标题" prop="title">
<el-input v-model="contentForm.title" placeholder="请输入内容标题" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="contentForm.content"
type="textarea"
:rows="6"
placeholder="请输入测评内容(课文或文字)"
maxlength="1000"
show-word-limit
/>
</el-form-item>
<el-form-item label="难度" prop="difficulty">
<el-radio-group v-model="contentForm.difficulty">
<el-radio label="easy">简单</el-radio>
<el-radio label="medium">中等</el-radio>
<el-radio label="hard">困难</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="contentForm.sortOrder" :min="0" :max="9999" />
<span style="margin-left: 10px; color: #909399; font-size: 12px;">数字越小越靠前</span>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="contentForm.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="contentForm.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="contentOpen = false">取 消</el-button>
<el-button type="primary" @click="submitContentForm">确 定</el-button>
</div>
</el-dialog>
<!-- 内容列表对话框(用于选择要编辑的内容) -->
<el-dialog title="选择要编辑的内容" :visible.sync="contentListOpen" width="900px" append-to-body>
<el-table v-loading="contentListLoading" :data="contentList" border>
<el-table-column label="ID" align="center" prop="id" width="80" />
<el-table-column label="标题" align="center" prop="title" :show-overflow-tooltip="true" />
<el-table-column label="内容预览" align="center" prop="content" :show-overflow-tooltip="true" width="300">
<template slot-scope="scope">
<span>{{ scope.row.content ? (scope.row.content.length > 50 ? scope.row.content.substring(0, 50) + '...' : scope.row.content) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="难度" align="center" prop="difficulty" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.difficulty === 'easy'" type="success" size="small">简单</el-tag>
<el-tag v-else-if="scope.row.difficulty === 'medium'" type="warning" size="small">中等</el-tag>
<el-tag v-else-if="scope.row.difficulty === 'hard'" type="danger" size="small">困难</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success" size="small">启用</el-tag>
<el-tag v-else type="info" size="small">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleEditContentItem(scope.row)"
v-hasPermi="['study:voiceEvaluationContent:edit']"
>编辑</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDeleteContent(scope.row)"
v-hasPermi="['study:voiceEvaluationContent:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="contentTotal > 0"
:total="contentTotal"
:page.sync="contentQueryParams.pageNum"
:limit.sync="contentQueryParams.pageSize"
@pagination="getContentList"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="contentListOpen = false">关 闭</el-button>
</div>
</el-dialog>
<!-- 查看对话框 -->
<el-dialog title="语音评测详情" :visible.sync="viewOpen" width="800px" append-to-body :close-on-click-modal="false" class="view-dialog">
<el-descriptions :column="2" border>
<el-descriptions-item label="评测ID">{{ form.id }}</el-descriptions-item>
<el-descriptions-item label="学员姓名">{{ form.studentName }}</el-descriptions-item>
<el-descriptions-item label="课程名称">{{ form.courseName }}</el-descriptions-item>
<el-descriptions-item label="评测时间">{{ parseTime(form.evaluationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="提交状态">
<el-tag v-if="form.isSubmitted === 1" type="success" size="small">已提交</el-tag>
<el-tag v-else type="info" size="small">未提交</el-tag>
</el-descriptions-item>
<el-descriptions-item label="提交时间">
<span v-if="form.submitTime">{{ parseTime(form.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
<span v-else style="color: #909399;">未提交</span>
</el-descriptions-item>
<el-descriptions-item label="评测内容" :span="2">
<div style="max-height: 100px; overflow-y: auto;">{{ form.content }}</div>
</el-descriptions-item>
<el-descriptions-item label="总分" :span="2">
<span style="color: #409eff; font-weight: bold; font-size: 18px;">{{ parseFloat(form.score || 0).toFixed(0) }}分</span>
</el-descriptions-item>
<el-descriptions-item label="准确度">{{ parseFloat(form.accuracy || 0).toFixed(0) }}分</el-descriptions-item>
<el-descriptions-item label="完整度">{{ parseFloat(form.completeness || 0).toFixed(0) }}分</el-descriptions-item>
<el-descriptions-item label="流畅度">{{ parseFloat(form.fluency || 0).toFixed(0) }}分</el-descriptions-item>
<el-descriptions-item label="发音">{{ parseFloat(form.pronunciation || 0).toFixed(0) }}分</el-descriptions-item>
<el-descriptions-item label="音频文件" :span="2">
<div style="margin-top: 10px;">
<div v-if="form.audioPath">
<div style="margin-bottom: 8px; color: #909399; font-size: 12px;">
<i class="el-icon-info"></i> 学生端提交的原始音频
</div>
<audio
:src="audioUrl"
controls
style="width: 100%;"
preload="metadata"
@error="handleAudioError"
@loadedmetadata="handleAudioLoaded"
>
您的浏览器不支持音频播放
</audio>
<div v-if="audioError" style="color: #f56c6c; font-size: 12px; margin-top: 5px;">
<i class="el-icon-warning"></i> 音频加载失败,请检查文件是否存在
</div>
<div style="margin-top: 8px; color: #909399; font-size: 12px; word-break: break-all;">
文件路径:{{ form.audioPath }}
</div>
</div>
<div v-else style="color: #f56c6c; font-size: 14px; padding: 20px; text-align: center; background-color: #fef0f0; border-radius: 4px;">
<i class="el-icon-warning"></i>
<div style="margin-top: 8px;">该记录没有保存音频文件路径</div>
<div style="margin-top: 5px; font-size: 12px; color: #909399;">
可能原因1. 这是旧数据,上传时未保存音频路径 2. 上传时出现错误
</div>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="详细结果" :span="2" v-if="form.resultDetail">
<pre class="result-detail-pre">{{ formatJson(form.resultDetail) }}</pre>
</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button @click="viewOpen = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listVoiceEvaluation, getVoiceEvaluation, delVoiceEvaluation, exportVoiceEvaluation,
listVoiceEvaluationContent, addVoiceEvaluationContent, updateVoiceEvaluationContent,
getVoiceEvaluationContent, delVoiceEvaluationContent } from "@/api/study/voiceEvaluation";
import { getFileUrl } from "@/utils/app";
export default {
name: "VoiceEvaluation",
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 语音评测表格数据
voiceEvaluationList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
viewOpen: false,
// 内容管理对话框
contentOpen: false,
// 内容列表对话框
contentListOpen: false,
// 内容列表加载状态
contentListLoading: false,
// 内容列表数据
contentList: [],
// 内容列表总数
contentTotal: 0,
// 内容列表查询参数
contentQueryParams: {
pageNum: 1,
pageSize: 10
},
// 内容表单
contentForm: {},
// 内容表单校验
contentRules: {
title: [
{ required: true, message: "标题不能为空", trigger: "blur" },
{ max: 100, message: "标题长度不能超过100个字符", trigger: "blur" }
],
content: [
{ required: true, message: "内容不能为空", trigger: "blur" },
{ max: 1000, message: "内容长度不能超过1000个字符", trigger: "blur" }
],
difficulty: [
{ required: true, message: "难度不能为空", trigger: "change" }
],
status: [
{ required: true, message: "状态不能为空", trigger: "change" }
]
},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: null,
courseName: null
},
// 表单参数
form: {},
// 表单校验
rules: {},
// 评测时间范围
daterangeEvaluationTime: [],
// 音频加载错误
audioError: false
};
},
computed: {
audioUrl() {
if (this.form.audioPath) {
// 如果已经是完整URLhttp://或https://开头),直接返回
if (this.form.audioPath.startsWith('http://') || this.form.audioPath.startsWith('https://')) {
return this.form.audioPath;
}
// 否则使用文件访问URL不包含/api前缀
return getFileUrl(this.form.audioPath);
}
return '';
}
},
created() {
this.getList();
},
methods: {
/** 查询语音评测列表 */
getList() {
this.loading = true;
this.queryParams.params = {};
if (null != this.daterangeEvaluationTime && '' != this.daterangeEvaluationTime) {
this.queryParams.params["beginTime"] = this.daterangeEvaluationTime[0];
this.queryParams.params["endTime"] = this.daterangeEvaluationTime[1];
}
listVoiceEvaluation(this.queryParams).then(response => {
this.voiceEvaluationList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.viewOpen = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
id: null,
studentId: null,
courseId: null,
content: null,
audioPath: null,
score: null,
accuracy: null,
fluency: null,
completeness: null,
pronunciation: null,
resultDetail: null,
evaluationTime: null,
isSubmitted: null,
submitTime: null
};
this.resetForm("form");
},
// 内容表单重置
resetContentForm() {
this.contentForm = {
id: null,
title: null,
content: null,
difficulty: 'medium',
sortOrder: 0,
status: 1,
remark: null
};
this.resetForm("contentForm");
},
/** 增加内容按钮操作 */
handleAddContent() {
this.resetContentForm();
this.contentOpen = true;
},
/** 编辑内容按钮操作 */
handleEditContent() {
this.contentListOpen = true;
this.contentQueryParams.pageNum = 1;
this.getContentList();
},
/** 查询内容列表 */
getContentList() {
this.contentListLoading = true;
listVoiceEvaluationContent(this.contentQueryParams).then(response => {
this.contentList = response.rows || [];
this.contentTotal = response.total || 0;
this.contentListLoading = false;
}).catch(() => {
this.contentListLoading = false;
});
},
/** 编辑内容项 */
handleEditContentItem(row) {
const id = row.id;
getVoiceEvaluationContent(id).then(response => {
this.contentForm = response.data || {};
this.contentListOpen = false;
this.contentOpen = true;
}).catch(() => {
this.$modal.msgError("获取内容详情失败");
});
},
/** 删除内容 */
handleDeleteContent(row) {
const id = row.id;
this.$modal.confirm('是否确认删除标题为"' + row.title + '"的内容?').then(() => {
return delVoiceEvaluationContent(id);
}).then(() => {
this.getContentList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 提交内容表单 */
submitContentForm() {
this.$refs["contentForm"].validate(valid => {
if (valid) {
if (this.contentForm.id != null) {
updateVoiceEvaluationContent(this.contentForm).then(response => {
this.$modal.msgSuccess("修改成功");
this.contentOpen = false;
this.resetContentForm();
// 如果内容列表对话框是打开的,刷新列表
if (this.contentListOpen) {
this.getContentList();
}
});
} else {
addVoiceEvaluationContent(this.contentForm).then(response => {
this.$modal.msgSuccess("新增成功");
this.contentOpen = false;
this.resetContentForm();
// 如果内容列表对话框是打开的,刷新列表
if (this.contentListOpen) {
this.getContentList();
}
});
}
}
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.daterangeEvaluationTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 查看按钮操作 */
handleView(row) {
const id = row.id || this.ids[0]
getVoiceEvaluation(id).then(response => {
this.form = response.data;
this.audioError = false; // 重置音频错误状态
// 调试:检查音频路径
console.log('语音评测详情数据:', {
id: this.form.id,
audioPath: this.form.audioPath,
audioUrl: this.audioUrl,
hasAudioPath: !!this.form.audioPath
});
// 如果 audioPath 为 null提示用户
if (!this.form.audioPath) {
console.warn('警告:该记录的音频路径为空,可能是旧数据或上传时未保存音频路径');
}
this.viewOpen = true;
this.title = "语音评测详情";
});
},
/** 音频加载错误处理 */
handleAudioError() {
this.audioError = true;
console.error('音频加载失败:', {
audioPath: this.form.audioPath,
audioUrl: this.audioUrl,
baseApi: process.env.VUE_APP_BASE_API
});
},
/** 音频加载成功处理 */
handleAudioLoaded() {
this.audioError = false;
console.log('音频加载成功:', this.audioUrl);
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除语音评测编号为"' + ids + '"的数据项?').then(function() {
return delVoiceEvaluation(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download('study/voiceEvaluation/export', {
...this.queryParams
}, `voiceEvaluation_${new Date().getTime()}.xlsx`)
},
/** 格式化JSON */
formatJson(jsonStr) {
if (!jsonStr) return '';
try {
const obj = JSON.parse(jsonStr);
return JSON.stringify(obj, null, 2);
} catch (e) {
return jsonStr;
}
}
}
};
</script>
<style scoped>
/* 详细结果区域样式 */
.result-detail-pre {
max-height: 200px;
overflow-y: auto;
background-color: #f5f7fa;
padding: 12px;
border-radius: 4px;
border: none;
margin: 0;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
font-family: Consolas, Monaco, 'Courier New', monospace;
}
</style>
<style>
/* 详情弹窗样式不使用scoped确保覆盖element-ui默认样式 */
.view-dialog.el-dialog__wrapper {
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 10vh;
}
.view-dialog .el-dialog {
margin: 0 !important;
}
.view-dialog .el-dialog__body {
padding: 20px;
max-height: 70vh;
overflow-y: auto;
}
</style>