|
|
|
@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
|
|
<div class="p-2">
|
|
|
|
|
|
|
|
<!-- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">-->
|
|
|
|
|
|
|
|
<!-- <div v-show="showSearch" class="mb-[10px]">-->
|
|
|
|
|
|
|
|
<!-- <el-card shadow="hover">-->
|
|
|
|
|
|
|
|
<!-- <el-form ref="queryFormRef" :model="queryParams" :inline="true" >-->
|
|
|
|
|
|
|
|
<!-- <el-form-item label="记录ID,主键" prop="asrRecordId">-->
|
|
|
|
|
|
|
|
<!-- <el-input v-model="queryParams.asrRecordId" placeholder="请输入记录ID,主键" clearable @keyup.enter="handleQuery" />-->
|
|
|
|
|
|
|
|
<!-- </el-form-item>-->
|
|
|
|
|
|
|
|
<!-- <el-form-item label="LLM对话模型ID,关联ai_model" prop="modelId">-->
|
|
|
|
|
|
|
|
<!-- <el-input v-model="queryParams.modelId" placeholder="请输入LLM对话模型ID,关联ai_model" clearable @keyup.enter="handleQuery" />-->
|
|
|
|
|
|
|
|
<!-- </el-form-item>-->
|
|
|
|
|
|
|
|
<!-- <el-form-item label="语音识别文件地址" prop="asrFileUrl">-->
|
|
|
|
|
|
|
|
<!-- <el-input v-model="queryParams.asrFileUrl" placeholder="请输入语音识别文件地址" clearable @keyup.enter="handleQuery" />-->
|
|
|
|
|
|
|
|
<!-- </el-form-item>-->
|
|
|
|
|
|
|
|
<!-- <el-form-item label="识别结果(1成功,0失败)" prop="asrResult">-->
|
|
|
|
|
|
|
|
<!-- <el-input v-model="queryParams.asrResult" placeholder="请输入识别结果(1成功,0失败)" clearable @keyup.enter="handleQuery" />-->
|
|
|
|
|
|
|
|
<!-- </el-form-item>-->
|
|
|
|
|
|
|
|
<!-- <el-form-item label="识别失败原因" prop="asrFailedReason">-->
|
|
|
|
|
|
|
|
<!-- <el-input v-model="queryParams.asrFailedReason" placeholder="请输入识别失败原因" clearable @keyup.enter="handleQuery" />-->
|
|
|
|
|
|
|
|
<!-- </el-form-item>-->
|
|
|
|
|
|
|
|
<!-- <el-form-item>-->
|
|
|
|
|
|
|
|
<!-- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>-->
|
|
|
|
|
|
|
|
<!-- <el-button icon="Refresh" @click="resetQuery">重置</el-button>-->
|
|
|
|
|
|
|
|
<!-- </el-form-item>-->
|
|
|
|
|
|
|
|
<!-- </el-form>-->
|
|
|
|
|
|
|
|
<!-- </el-card>-->
|
|
|
|
|
|
|
|
<!-- </div>-->
|
|
|
|
|
|
|
|
<!-- </transition>-->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<el-table v-loading="loading" :data="asrRecordList" @selection-change="handleSelectionChange">
|
|
|
|
|
|
|
|
<!-- <el-table-column type="selection" width="55" align="center" />-->
|
|
|
|
|
|
|
|
<el-table-column label="记录ID,主键" align="center" prop="asrRecordId" v-if="columns[0].visible"/>
|
|
|
|
|
|
|
|
<el-table-column label="模型名称" align="center" prop="modelName" v-if="columns[2].visible"/>
|
|
|
|
|
|
|
|
<el-table-column label="语音识别文件地址" align="center" prop="asrFileUrl" v-if="columns[3].visible"/>
|
|
|
|
|
|
|
|
<el-table-column label="语音识别内容" align="center" prop="asrContent" v-if="columns[4].visible"/>
|
|
|
|
|
|
|
|
<el-table-column label="识别结果" align="center" prop="asrResult" v-if="columns[5].visible">
|
|
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
|
|
<dict-tag :options='ai_asr_result' :value='scope.row.asrResult'/>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
<el-table-column label="识别失败原因" align="center" prop="asrFailedReason" v-if="columns[6].visible"/>
|
|
|
|
|
|
|
|
<el-table-column label="音频播放" align="center" width="300" v-if="columns[7].visible">
|
|
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
|
|
<div class="audio-player">
|
|
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
|
|
:type="currentPlayingId === scope.row.asrRecordId ? 'danger' : 'primary'"
|
|
|
|
|
|
|
|
:icon="currentPlayingId === scope.row.asrRecordId ? 'VideoPause' : 'VideoPlay'"
|
|
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
|
|
@click="togglePlay(scope.row)"
|
|
|
|
|
|
|
|
:loading="audioLoading"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{{ currentPlayingId === scope.row.asrRecordId ? '停止' : '播放' }}
|
|
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
<div v-if="currentPlayingId === scope.row.asrRecordId" class="audio-controls">
|
|
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
|
|
v-model="currentTime"
|
|
|
|
|
|
|
|
:max="duration"
|
|
|
|
|
|
|
|
:format-tooltip="formatTime"
|
|
|
|
|
|
|
|
@change="seekTo"
|
|
|
|
|
|
|
|
:disabled="!audioElement"
|
|
|
|
|
|
|
|
class="audio-slider"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div class="time-display">
|
|
|
|
|
|
|
|
<span>{{ formatTime(currentTime) }}</span>
|
|
|
|
|
|
|
|
<span>/</span>
|
|
|
|
|
|
|
|
<span>{{ formatTime(duration) }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
|
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script setup name="AsrRecord" lang="ts">
|
|
|
|
|
|
|
|
import { listAsrRecord, getAsrRecord, delAsrRecord, addAsrRecord, updateAsrRecord } from '@/api/ai/record/asrRecord';
|
|
|
|
|
|
|
|
import { AsrRecordVO, AsrRecordQuery, AsrRecordForm } from '@/api/ai/record/asrRecord/types';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
|
|
|
const {ai_asr_result} = toRefs<any>(proxy?.useDict('ai_asr_result'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const asrRecordList = ref<AsrRecordVO[]>([]);
|
|
|
|
|
|
|
|
const buttonLoading = ref(false);
|
|
|
|
|
|
|
|
const loading = ref(true);
|
|
|
|
|
|
|
|
const showSearch = ref(true);
|
|
|
|
|
|
|
|
const ids = ref<Array<string | number>>([]);
|
|
|
|
|
|
|
|
const single = ref(true);
|
|
|
|
|
|
|
|
const multiple = ref(true);
|
|
|
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 音频播放相关状态
|
|
|
|
|
|
|
|
const audioElement = ref<HTMLAudioElement | null>(null);
|
|
|
|
|
|
|
|
const currentPlayingId = ref<string | number | null>(null);
|
|
|
|
|
|
|
|
const currentTime = ref(0);
|
|
|
|
|
|
|
|
const duration = ref(0);
|
|
|
|
|
|
|
|
const audioLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queryFormRef = ref<ElFormInstance>();
|
|
|
|
|
|
|
|
const asrRecordFormRef = ref<ElFormInstance>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dialog = reactive<DialogOption>({
|
|
|
|
|
|
|
|
visible: false,
|
|
|
|
|
|
|
|
title: ''
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 列显隐信息
|
|
|
|
|
|
|
|
const columns = ref<FieldOption[]>([
|
|
|
|
|
|
|
|
{ key: 0, label: `记录ID,主键`, visible: false },
|
|
|
|
|
|
|
|
{ key: 1, label: `租户ID`, visible: true },
|
|
|
|
|
|
|
|
{ key: 2, label: `模型名称`, visible: true },
|
|
|
|
|
|
|
|
{ key: 3, label: `语音识别文件地址`, visible: true },
|
|
|
|
|
|
|
|
{ key: 4, label: `语音识别内容`, visible: true },
|
|
|
|
|
|
|
|
{ key: 5, label: `识别结果`, visible: true },
|
|
|
|
|
|
|
|
{ key: 6, label: `识别失败原因`, visible: true },
|
|
|
|
|
|
|
|
{ key: 7, label: `音频播放`, visible: true },
|
|
|
|
|
|
|
|
{ key: 8, label: `创建部门`, visible: true },
|
|
|
|
|
|
|
|
{ key: 9, label: `创建人`, visible: true },
|
|
|
|
|
|
|
|
{ key: 10, label: `创建时间`, visible: true },
|
|
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const initFormData: AsrRecordForm = {
|
|
|
|
|
|
|
|
asrRecordId: undefined,
|
|
|
|
|
|
|
|
modelId: undefined,
|
|
|
|
|
|
|
|
asrFileUrl: undefined,
|
|
|
|
|
|
|
|
asrContent: undefined,
|
|
|
|
|
|
|
|
asrResult: undefined,
|
|
|
|
|
|
|
|
asrFailedReason: undefined,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = reactive<PageData<AsrRecordForm, AsrRecordQuery>>({
|
|
|
|
|
|
|
|
form: {...initFormData},
|
|
|
|
|
|
|
|
queryParams: {
|
|
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
|
|
asrRecordId: undefined,
|
|
|
|
|
|
|
|
modelId: undefined,
|
|
|
|
|
|
|
|
asrFileUrl: undefined,
|
|
|
|
|
|
|
|
asrContent: undefined,
|
|
|
|
|
|
|
|
asrResult: undefined,
|
|
|
|
|
|
|
|
asrFailedReason: undefined,
|
|
|
|
|
|
|
|
params: {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
rules: {
|
|
|
|
|
|
|
|
asrRecordId: [
|
|
|
|
|
|
|
|
{ required: true, message: "记录ID,主键不能为空", trigger: "blur" }
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
modelId: [
|
|
|
|
|
|
|
|
{ required: true, message: "LLM对话模型ID,关联ai_model不能为空", trigger: "blur" }
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
asrFileUrl: [
|
|
|
|
|
|
|
|
{ required: true, message: "语音识别文件地址不能为空", trigger: "blur" }
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
asrContent: [
|
|
|
|
|
|
|
|
{ required: true, message: "语音识别内容不能为空", trigger: "blur" }
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
asrResult: [
|
|
|
|
|
|
|
|
{ required: true, message: "识别结果(1成功,0失败)不能为空", trigger: "blur" }
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
asrFailedReason: [
|
|
|
|
|
|
|
|
{ required: true, message: "识别失败原因不能为空", trigger: "blur" }
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { queryParams, form, rules } = toRefs(data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 查询AI语音识别记录列表 */
|
|
|
|
|
|
|
|
const getList = async () => {
|
|
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
|
|
const res = await listAsrRecord(queryParams.value);
|
|
|
|
|
|
|
|
asrRecordList.value = res.rows;
|
|
|
|
|
|
|
|
total.value = res.total;
|
|
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 取消按钮 */
|
|
|
|
|
|
|
|
const cancel = () => {
|
|
|
|
|
|
|
|
reset();
|
|
|
|
|
|
|
|
dialog.visible = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 表单重置 */
|
|
|
|
|
|
|
|
const reset = () => {
|
|
|
|
|
|
|
|
form.value = {...initFormData};
|
|
|
|
|
|
|
|
asrRecordFormRef.value?.resetFields();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 搜索按钮操作 */
|
|
|
|
|
|
|
|
const handleQuery = () => {
|
|
|
|
|
|
|
|
queryParams.value.pageNum = 1;
|
|
|
|
|
|
|
|
getList();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 重置按钮操作 */
|
|
|
|
|
|
|
|
const resetQuery = () => {
|
|
|
|
|
|
|
|
queryFormRef.value?.resetFields();
|
|
|
|
|
|
|
|
handleQuery();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 多选框选中数据 */
|
|
|
|
|
|
|
|
const handleSelectionChange = (selection: AsrRecordVO[]) => {
|
|
|
|
|
|
|
|
ids.value = selection.map(item => item.asrRecordId);
|
|
|
|
|
|
|
|
single.value = selection.length != 1;
|
|
|
|
|
|
|
|
multiple.value = !selection.length;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 新增按钮操作 */
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
|
|
|
|
|
reset();
|
|
|
|
|
|
|
|
dialog.visible = true;
|
|
|
|
|
|
|
|
dialog.title = "添加AI语音识别记录";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 修改按钮操作 */
|
|
|
|
|
|
|
|
const handleUpdate = async (row?: AsrRecordVO) => {
|
|
|
|
|
|
|
|
reset();
|
|
|
|
|
|
|
|
const _asrRecordId = row?.asrRecordId || ids.value[0]
|
|
|
|
|
|
|
|
const res = await getAsrRecord(_asrRecordId);
|
|
|
|
|
|
|
|
Object.assign(form.value, res.data);
|
|
|
|
|
|
|
|
dialog.visible = true;
|
|
|
|
|
|
|
|
dialog.title = "修改AI语音识别记录";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 提交按钮 */
|
|
|
|
|
|
|
|
const submitForm = () => {
|
|
|
|
|
|
|
|
asrRecordFormRef.value?.validate(async (valid: boolean) => {
|
|
|
|
|
|
|
|
if (valid) {
|
|
|
|
|
|
|
|
buttonLoading.value = true;
|
|
|
|
|
|
|
|
if (form.value.asrRecordId) {
|
|
|
|
|
|
|
|
await updateAsrRecord(form.value).finally(() => buttonLoading.value = false);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
await addAsrRecord(form.value).finally(() => buttonLoading.value = false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
proxy?.$modal.msgSuccess("操作成功");
|
|
|
|
|
|
|
|
dialog.visible = false;
|
|
|
|
|
|
|
|
await getList();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 删除按钮操作 */
|
|
|
|
|
|
|
|
const handleDelete = async (row?: AsrRecordVO) => {
|
|
|
|
|
|
|
|
const _asrRecordIds = row?.asrRecordId || ids.value;
|
|
|
|
|
|
|
|
await proxy?.$modal.confirm('是否确认删除AI语音识别记录编号为"' + _asrRecordIds + '"的数据项?').finally(() => loading.value = false);
|
|
|
|
|
|
|
|
await delAsrRecord(_asrRecordIds);
|
|
|
|
|
|
|
|
proxy?.$modal.msgSuccess("删除成功");
|
|
|
|
|
|
|
|
await getList();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 导出按钮操作 */
|
|
|
|
|
|
|
|
const handleExport = () => {
|
|
|
|
|
|
|
|
proxy?.download('ai/asrRecord/export', {
|
|
|
|
|
|
|
|
...queryParams.value
|
|
|
|
|
|
|
|
}, `asrRecord_${new Date().getTime()}.xlsx`)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 音频播放相关方法
|
|
|
|
|
|
|
|
const togglePlay = async (row: AsrRecordVO) => {
|
|
|
|
|
|
|
|
if (currentPlayingId.value === row.asrRecordId) {
|
|
|
|
|
|
|
|
// 停止播放
|
|
|
|
|
|
|
|
stopAudio();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 开始播放
|
|
|
|
|
|
|
|
await playAudio(row);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const playAudio = async (row: AsrRecordVO) => {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
audioLoading.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 停止当前播放的音频
|
|
|
|
|
|
|
|
if (audioElement.value) {
|
|
|
|
|
|
|
|
audioElement.value.pause();
|
|
|
|
|
|
|
|
audioElement.value = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的音频元素
|
|
|
|
|
|
|
|
audioElement.value = new Audio(row.asrFileUrl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 设置事件监听
|
|
|
|
|
|
|
|
audioElement.value.addEventListener('loadedmetadata', () => {
|
|
|
|
|
|
|
|
duration.value = audioElement.value?.duration || 0;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
audioElement.value.addEventListener('timeupdate', () => {
|
|
|
|
|
|
|
|
currentTime.value = audioElement.value?.currentTime || 0;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
audioElement.value.addEventListener('ended', () => {
|
|
|
|
|
|
|
|
stopAudio();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
audioElement.value.addEventListener('error', (e) => {
|
|
|
|
|
|
|
|
console.error('音频播放错误:', e);
|
|
|
|
|
|
|
|
proxy?.$modal.msgError('音频播放失败');
|
|
|
|
|
|
|
|
stopAudio();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 开始播放
|
|
|
|
|
|
|
|
await audioElement.value.play();
|
|
|
|
|
|
|
|
currentPlayingId.value = row.asrRecordId;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('播放音频失败:', error);
|
|
|
|
|
|
|
|
proxy?.$modal.msgError('播放音频失败');
|
|
|
|
|
|
|
|
stopAudio();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
audioLoading.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stopAudio = () => {
|
|
|
|
|
|
|
|
if (audioElement.value) {
|
|
|
|
|
|
|
|
audioElement.value.pause();
|
|
|
|
|
|
|
|
audioElement.value.currentTime = 0;
|
|
|
|
|
|
|
|
audioElement.value = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPlayingId.value = null;
|
|
|
|
|
|
|
|
currentTime.value = 0;
|
|
|
|
|
|
|
|
duration.value = 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seekTo = (value: number) => {
|
|
|
|
|
|
|
|
if (audioElement.value) {
|
|
|
|
|
|
|
|
audioElement.value.currentTime = value;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const formatTime = (seconds: number) => {
|
|
|
|
|
|
|
|
if (!seconds || isNaN(seconds)) return '00:00';
|
|
|
|
|
|
|
|
const mins = Math.floor(seconds / 60);
|
|
|
|
|
|
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
|
|
|
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
|
|
getList();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
// 清理音频资源
|
|
|
|
|
|
|
|
stopAudio();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
.audio-player {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
min-width: 250px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.audio-controls {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.audio-slider {
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
margin: 0 8px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.time-display {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.time-display span {
|
|
|
|
|
|
|
|
min-width: 35px;
|
|
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|