946 lines
24 KiB
Vue
946 lines
24 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="page">
|
|||
|
|
<u-navbar title="语音详情" :border-bottom="false" :background="bgc" back-icon-color="#262B37" title-color='#262B37'
|
|||
|
|
title-size='36' height='36' id="navbar">
|
|||
|
|
</u-navbar>
|
|||
|
|
|
|||
|
|
<!-- <scroll-view class="detail-container" scroll-y v-if="!isEdit">
|
|||
|
|
<view class="detail-section">
|
|||
|
|
<view class="detail-item">
|
|||
|
|
<view class="detail-label">语音编号</view>
|
|||
|
|
<view class="detail-value">{{ voiceDetail.no || '-' }}</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="detail-item">
|
|||
|
|
<view class="detail-label">运营区</view>
|
|||
|
|
<view class="detail-value">{{ voiceDetail.areaName || '-' }}</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="detail-item" v-if="voiceDetail.remark">
|
|||
|
|
<view class="detail-label">备注</view>
|
|||
|
|
<view class="detail-value">{{ voiceDetail.remark }}</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="detail-item" v-if="voiceDetail.fileUrl">
|
|||
|
|
<view class="detail-label">语音文件</view>
|
|||
|
|
<view class="detail-value file-url" @click="playAudio">
|
|||
|
|
<u-icon name="play-circle" color="#3996FD" size="32"></u-icon>
|
|||
|
|
<text>点击播放</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="action-section">
|
|||
|
|
<view class="action-btn edit-btn" @click="toEdit">
|
|||
|
|
修改
|
|||
|
|
</view>
|
|||
|
|
<view class="action-btn delete-btn" @click="deleteVoice">
|
|||
|
|
删除
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view> -->
|
|||
|
|
|
|||
|
|
<!-- 编辑表单:与新增页一致,无语音名称/运营区,编号弹窗+语音预览 -->
|
|||
|
|
<scroll-view class="form-container" scroll-y>
|
|||
|
|
<view class="form-card">
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">
|
|||
|
|
<text class="label-text">语音文件</text>
|
|||
|
|
<text class="required">*</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="upload-box" @click="!editForm.fileUrl && uploadFile()">
|
|||
|
|
<view v-if="!editForm.fileUrl" class="upload-placeholder">
|
|||
|
|
<u-icon name="plus-circle" color="#3996FD" size="64"></u-icon>
|
|||
|
|
<text class="upload-tip">点击上传MP3文件</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-else class="audio-bar">
|
|||
|
|
<view class="audio-remove" @click.stop="removeFile">
|
|||
|
|
<u-icon name="close-circle-fill" color="#ff4444" size="36"></u-icon>
|
|||
|
|
</view>
|
|||
|
|
<view class="audio-play-btn" @click="togglePreviewPlay">
|
|||
|
|
<u-icon
|
|||
|
|
:name="previewPlaying ? 'pause-circle-fill' : 'play-circle-fill'"
|
|||
|
|
:color="previewPlaying ? '#64B6A7' : '#3996FD'"
|
|||
|
|
size="52"
|
|||
|
|
></u-icon>
|
|||
|
|
</view>
|
|||
|
|
<view class="audio-progress-wrap">
|
|||
|
|
<slider
|
|||
|
|
class="audio-slider"
|
|||
|
|
:value="previewProgress"
|
|||
|
|
:min="0"
|
|||
|
|
:max="100"
|
|||
|
|
:step="0.1"
|
|||
|
|
activeColor="#3996FD"
|
|||
|
|
backgroundColor="#E5E7EB"
|
|||
|
|
@change="onPreviewSeek"
|
|||
|
|
@changing="onPreviewSeekChanging"
|
|||
|
|
/>
|
|||
|
|
<view class="audio-time">
|
|||
|
|
<text>{{ formatTime(previewCurrentTime) }}</text>
|
|||
|
|
<text>/ {{ formatTime(previewDuration) }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="form-tip">仅支持MP3格式文件</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="form-card">
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">
|
|||
|
|
<text class="label-text">语音编号</text>
|
|||
|
|
<text class="required">*</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="picker-box" @click="openNoPicker">
|
|||
|
|
<text :class="['picker-text', editForm.no ? '' : 'placeholder']">
|
|||
|
|
{{ editForm.no || '请选择语音编号(11-40)' }}
|
|||
|
|
</text>
|
|||
|
|
<u-icon name="arrow-right" color="#C0C4CC" size="20"></u-icon>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="form-card">
|
|||
|
|
<view class="form-item">
|
|||
|
|
<view class="form-label">
|
|||
|
|
<text class="label-text">备注</text>
|
|||
|
|
<text class="label-tip">(可选)</text>
|
|||
|
|
</view>
|
|||
|
|
<textarea
|
|||
|
|
class="form-textarea"
|
|||
|
|
v-model="editForm.remark"
|
|||
|
|
placeholder="请输入备注信息"
|
|||
|
|
maxlength="200"
|
|||
|
|
:auto-height="true"
|
|||
|
|
></textarea>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="form-actions">
|
|||
|
|
<view class="form-btn cancel-btn" style="background: red;color: #fff;" @click="deleteVoice">删除</view>
|
|||
|
|
<view class="form-btn submit-btn" @click="submitEdit">保存</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 语音编号选择弹窗:与新增页一致 -->
|
|||
|
|
<view class="no-picker-mask" v-if="showNoPicker" @click="closeNoPicker"></view>
|
|||
|
|
<view class="no-picker-modal" v-if="showNoPicker" @click.stop="">
|
|||
|
|
<view class="no-picker-title">选择语音编号</view>
|
|||
|
|
<scroll-view class="no-picker-list" scroll-y>
|
|||
|
|
<view
|
|||
|
|
class="no-picker-item"
|
|||
|
|
:class="{ active: editForm.no === item.label, disabled: isNoConfigured(item.label) }"
|
|||
|
|
v-for="(item, idx) in noOptions"
|
|||
|
|
:key="idx"
|
|||
|
|
@click="selectNo(idx)"
|
|||
|
|
>
|
|||
|
|
<text>{{ item.label }}</text>
|
|||
|
|
<view class="no-picker-right">
|
|||
|
|
<text class="configured-tag" v-if="isNoConfigured(item.label)">已配置</text>
|
|||
|
|
<u-icon v-else-if="editForm.no === item.label" name="checkmark" color="#3996FD" size="20"></u-icon>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
<view class="no-picker-cancel" @click="closeNoPicker">取消</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
bgc: {
|
|||
|
|
backgroundColor: "#fff",
|
|||
|
|
},
|
|||
|
|
voiceId: '',
|
|||
|
|
voiceDetail: {},
|
|||
|
|
isEdit: false,
|
|||
|
|
noOptions: [],
|
|||
|
|
noIndex: -1,
|
|||
|
|
editForm: {
|
|||
|
|
fileUrl: '',
|
|||
|
|
no: '',
|
|||
|
|
areaId: '',
|
|||
|
|
remark: ''
|
|||
|
|
},
|
|||
|
|
fileName: '',
|
|||
|
|
token: '',
|
|||
|
|
upurl: '',
|
|||
|
|
showNoPicker: false,
|
|||
|
|
configuredNoSet: {},
|
|||
|
|
previewAudio: null,
|
|||
|
|
previewPlaying: false,
|
|||
|
|
previewProgress: 0,
|
|||
|
|
previewCurrentTime: 0,
|
|||
|
|
previewDuration: 0,
|
|||
|
|
previewSeekChanging: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad(options) {
|
|||
|
|
if (options.id) {
|
|||
|
|
this.voiceId = options.id
|
|||
|
|
this.initNoOptions()
|
|||
|
|
this.getQiniuToken()
|
|||
|
|
this.getDetail()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onUnload() {
|
|||
|
|
this.destroyPreviewAudio()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
// 初始化编号选项
|
|||
|
|
initNoOptions() {
|
|||
|
|
for (let i = 11; i <= 40; i++) {
|
|||
|
|
this.noOptions.push({
|
|||
|
|
value: i,
|
|||
|
|
label: i.toString()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// 获取详情
|
|||
|
|
getDetail() {
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: '加载中...',
|
|||
|
|
mask: true
|
|||
|
|
})
|
|||
|
|
this.$u.get(`/bst/voice/${this.voiceId}`).then((res) => {
|
|||
|
|
if (res.code == 200) {
|
|||
|
|
this.voiceDetail = res.data || {}
|
|||
|
|
this.editForm = {
|
|||
|
|
fileUrl: this.voiceDetail.fileUrl || '',
|
|||
|
|
no: this.voiceDetail.no != null ? this.voiceDetail.no.toString() : '',
|
|||
|
|
areaId: this.voiceDetail.areaId || '',
|
|||
|
|
remark: this.voiceDetail.remark || ''
|
|||
|
|
}
|
|||
|
|
this.fileName = this.voiceDetail.fileUrl ? 'audio.mp3' : ''
|
|||
|
|
const noIdx = this.noOptions.findIndex(item => item.label === this.editForm.no)
|
|||
|
|
if (noIdx >= 0) this.noIndex = noIdx
|
|||
|
|
this.loadConfiguredNos()
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: res.msg || '获取详情失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}).finally(() => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 拉取当前运营区已配置编号,编辑时排除当前语音的 no
|
|||
|
|
loadConfiguredNos() {
|
|||
|
|
const areaId = this.voiceDetail.areaId || this.editForm.areaId
|
|||
|
|
if (!areaId) return
|
|||
|
|
this.$u.get('/bst/voice/list?areaId=' + areaId).then(res => {
|
|||
|
|
if (res.code != 200) return
|
|||
|
|
const rows = Array.isArray(res.data) ? res.data : (res.rows || [])
|
|||
|
|
const set = {}
|
|||
|
|
rows.forEach(item => {
|
|||
|
|
if (item && item.id == this.voiceId) return
|
|||
|
|
const no = item && (item.no != null ? String(item.no) : '')
|
|||
|
|
if (no) set[no] = true
|
|||
|
|
})
|
|||
|
|
this.configuredNoSet = set
|
|||
|
|
this.$forceUpdate()
|
|||
|
|
}).catch(() => {})
|
|||
|
|
},
|
|||
|
|
isNoConfigured(label) {
|
|||
|
|
return !!(this.configuredNoSet && this.configuredNoSet[String(label)])
|
|||
|
|
},
|
|||
|
|
openNoPicker() {
|
|||
|
|
if (this.voiceDetail.areaId && (!this.configuredNoSet || Object.keys(this.configuredNoSet).length === 0)) {
|
|||
|
|
this.loadConfiguredNos()
|
|||
|
|
}
|
|||
|
|
this.showNoPicker = true
|
|||
|
|
},
|
|||
|
|
closeNoPicker() {
|
|||
|
|
this.showNoPicker = false
|
|||
|
|
},
|
|||
|
|
selectNo(index) {
|
|||
|
|
const item = this.noOptions[index]
|
|||
|
|
if (item && this.isNoConfigured(item.label)) {
|
|||
|
|
uni.showToast({ title: '该编号已配置', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.noIndex = index
|
|||
|
|
this.editForm.no = this.noOptions[index].label
|
|||
|
|
this.showNoPicker = false
|
|||
|
|
},
|
|||
|
|
// 获取七牛云token
|
|||
|
|
getQiniuToken() {
|
|||
|
|
this.$u.get("/common/qiniuToken").then((res) => {
|
|||
|
|
if (res.code == 200) {
|
|||
|
|
this.token = res.data
|
|||
|
|
this.upurl = res.domain
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 播放音频
|
|||
|
|
playAudio() {
|
|||
|
|
if (this.voiceDetail.fileUrl) {
|
|||
|
|
// #ifdef H5
|
|||
|
|
const audio = new Audio(this.voiceDetail.fileUrl)
|
|||
|
|
audio.play()
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef H5
|
|||
|
|
uni.downloadFile({
|
|||
|
|
url: this.voiceDetail.fileUrl,
|
|||
|
|
success: (res) => {
|
|||
|
|
uni.openDocument({
|
|||
|
|
filePath: res.tempFilePath,
|
|||
|
|
success: () => {
|
|||
|
|
console.log('打开文档成功')
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// 进入编辑模式
|
|||
|
|
toEdit() {
|
|||
|
|
this.isEdit = true
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
if (this.editForm.fileUrl) this.preloadPreviewDuration()
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 取消编辑
|
|||
|
|
cancelEdit() {
|
|||
|
|
this.destroyPreviewAudio()
|
|||
|
|
this.isEdit = false
|
|||
|
|
this.getDetail()
|
|||
|
|
},
|
|||
|
|
// 上传文件
|
|||
|
|
uploadFile() {
|
|||
|
|
if (!this.token) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '上传token获取失败,请重试',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// #ifdef H5
|
|||
|
|
// H5使用input选择文件
|
|||
|
|
const input = document.createElement('input')
|
|||
|
|
input.type = 'file'
|
|||
|
|
input.accept = 'audio/mp3,audio/mpeg'
|
|||
|
|
input.onchange = (e) => {
|
|||
|
|
const file = e.target.files[0]
|
|||
|
|
if (file) {
|
|||
|
|
if (!file.name.toLowerCase().endsWith('.mp3')) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '仅支持MP3格式文件',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.uploadToQiniu(file)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
input.click()
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
// 微信小程序使用wx.chooseMessageFile选择文件
|
|||
|
|
wx.chooseMessageFile({
|
|||
|
|
count: 1,
|
|||
|
|
type: 'file',
|
|||
|
|
success: (res) => {
|
|||
|
|
const file = res.tempFiles[0]
|
|||
|
|
if (file) {
|
|||
|
|
// 检查文件类型
|
|||
|
|
if (!file.name.toLowerCase().endsWith('.mp3')) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '仅支持MP3格式文件',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.uploadToQiniu(file.path)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
console.log('选择文件失败', err)
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '选择文件失败,请从聊天中选择文件',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef H5 || MP-WEIXIN
|
|||
|
|
// 其他平台(如APP)使用uni.chooseFile
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
uni.chooseFile({
|
|||
|
|
count: 1,
|
|||
|
|
type: 'file',
|
|||
|
|
extension: ['.mp3'],
|
|||
|
|
success: (res) => {
|
|||
|
|
const file = res.tempFiles[0]
|
|||
|
|
if (file) {
|
|||
|
|
this.uploadToQiniu(file.path)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
console.log('选择文件失败', err)
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '选择文件失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
// 上传到七牛云
|
|||
|
|
uploadToQiniu(filePath) {
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: '上传中...',
|
|||
|
|
mask: true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
let _this = this
|
|||
|
|
let math = 'static/' + _this.$u.guid(20) + '.mp3'
|
|||
|
|
|
|||
|
|
// #ifndef H5
|
|||
|
|
wx.uploadFile({
|
|||
|
|
url: 'https://up-z2.qiniup.com',
|
|||
|
|
name: 'file',
|
|||
|
|
filePath: filePath,
|
|||
|
|
formData: {
|
|||
|
|
token: _this.token,
|
|||
|
|
key: 'bike/audio/' + math
|
|||
|
|
},
|
|||
|
|
success: function(res) {
|
|||
|
|
try {
|
|||
|
|
let str = JSON.parse(res.data)
|
|||
|
|
_this.destroyPreviewAudio()
|
|||
|
|
_this.editForm.fileUrl = _this.upurl + '/' + str.key
|
|||
|
|
_this.fileName = 'audio.mp3'
|
|||
|
|
_this.preloadPreviewDuration()
|
|||
|
|
uni.hideLoading()
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '上传成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
} catch(e) {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '上传失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: function() {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '上传失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifdef H5
|
|||
|
|
const reader = new FileReader()
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
const blob = e.target.result
|
|||
|
|
const formData = new FormData()
|
|||
|
|
formData.append('file', blob)
|
|||
|
|
formData.append('token', _this.token)
|
|||
|
|
formData.append('key', 'bike/audio/' + math)
|
|||
|
|
|
|||
|
|
fetch('https://up-z2.qiniup.com', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: formData
|
|||
|
|
}).then(res => res.json()).then(data => {
|
|||
|
|
_this.destroyPreviewAudio()
|
|||
|
|
_this.editForm.fileUrl = _this.upurl + '/' + data.key
|
|||
|
|
_this.fileName = filePath.name || 'audio.mp3'
|
|||
|
|
_this.preloadPreviewDuration()
|
|||
|
|
uni.hideLoading()
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '上传成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}).catch(err => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '上传失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
reader.readAsArrayBuffer(filePath)
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
// 移除文件
|
|||
|
|
removeFile() {
|
|||
|
|
this.destroyPreviewAudio()
|
|||
|
|
this.editForm.fileUrl = ''
|
|||
|
|
this.fileName = ''
|
|||
|
|
},
|
|||
|
|
togglePreviewPlay() {
|
|||
|
|
if (!this.editForm.fileUrl) return
|
|||
|
|
if (this.previewPlaying) this.pausePreview()
|
|||
|
|
else this.playPreview()
|
|||
|
|
},
|
|||
|
|
playPreview() {
|
|||
|
|
if (!this.editForm.fileUrl) return
|
|||
|
|
if (this.previewAudio) {
|
|||
|
|
this.previewAudio.play()
|
|||
|
|
this.previewPlaying = true
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const ctx = uni.createInnerAudioContext()
|
|||
|
|
ctx.src = this.editForm.fileUrl
|
|||
|
|
ctx.autoplay = false
|
|||
|
|
ctx.onTimeUpdate(() => {
|
|||
|
|
if (this.previewSeekChanging) return
|
|||
|
|
this.previewCurrentTime = ctx.currentTime
|
|||
|
|
this.previewDuration = ctx.duration || 0
|
|||
|
|
if (ctx.duration > 0) this.previewProgress = (ctx.currentTime / ctx.duration) * 100
|
|||
|
|
})
|
|||
|
|
ctx.onEnded(() => {
|
|||
|
|
this.previewPlaying = false
|
|||
|
|
this.previewProgress = 0
|
|||
|
|
this.previewCurrentTime = 0
|
|||
|
|
})
|
|||
|
|
ctx.onError(() => {
|
|||
|
|
this.previewPlaying = false
|
|||
|
|
uni.showToast({ title: '播放失败', icon: 'none' })
|
|||
|
|
})
|
|||
|
|
ctx.onCanplay(() => {
|
|||
|
|
if (ctx.duration > 0) this.previewDuration = ctx.duration
|
|||
|
|
})
|
|||
|
|
ctx.play()
|
|||
|
|
this.previewAudio = ctx
|
|||
|
|
this.previewPlaying = true
|
|||
|
|
},
|
|||
|
|
pausePreview() {
|
|||
|
|
if (this.previewAudio) {
|
|||
|
|
this.previewAudio.pause()
|
|||
|
|
this.previewPlaying = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onPreviewSeekChanging() {
|
|||
|
|
this.previewSeekChanging = true
|
|||
|
|
},
|
|||
|
|
onPreviewSeek(e) {
|
|||
|
|
const p = Number(e.detail.value) || 0
|
|||
|
|
this.previewSeekChanging = false
|
|||
|
|
if (!this.previewAudio || !this.editForm.fileUrl) return
|
|||
|
|
const duration = this.previewAudio.duration || this.previewDuration
|
|||
|
|
if (duration <= 0) return
|
|||
|
|
const seekTo = (p / 100) * duration
|
|||
|
|
if (typeof this.previewAudio.seek === 'function') this.previewAudio.seek(seekTo)
|
|||
|
|
this.previewCurrentTime = seekTo
|
|||
|
|
this.previewProgress = p
|
|||
|
|
},
|
|||
|
|
formatTime(seconds) {
|
|||
|
|
if (seconds == null || isNaN(seconds) || seconds < 0) return '00:00'
|
|||
|
|
const s = Math.floor(seconds)
|
|||
|
|
const m = Math.floor(s / 60)
|
|||
|
|
const sec = s % 60
|
|||
|
|
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
|
|||
|
|
},
|
|||
|
|
destroyPreviewAudio() {
|
|||
|
|
if (this.previewAudio) {
|
|||
|
|
this.previewAudio.stop()
|
|||
|
|
this.previewAudio.destroy()
|
|||
|
|
this.previewAudio = null
|
|||
|
|
}
|
|||
|
|
this.previewPlaying = false
|
|||
|
|
this.previewProgress = 0
|
|||
|
|
this.previewCurrentTime = 0
|
|||
|
|
this.previewDuration = 0
|
|||
|
|
},
|
|||
|
|
// 与语音列表 index 预加载时长一致:只创建上下文设 src,不 play,在 onCanplay/onTimeUpdate 取 duration
|
|||
|
|
preloadPreviewDuration() {
|
|||
|
|
if (!this.editForm.fileUrl) return
|
|||
|
|
const audioContext = uni.createInnerAudioContext()
|
|||
|
|
audioContext.src = this.editForm.fileUrl
|
|||
|
|
audioContext.autoplay = false
|
|||
|
|
let hasGotDuration = false
|
|||
|
|
const trySetDuration = () => {
|
|||
|
|
if (hasGotDuration) return
|
|||
|
|
if (audioContext.duration > 0) {
|
|||
|
|
hasGotDuration = true
|
|||
|
|
this.previewDuration = audioContext.duration
|
|||
|
|
this.$forceUpdate()
|
|||
|
|
setTimeout(() => {
|
|||
|
|
try { audioContext.destroy() } catch (e) {}
|
|||
|
|
}, 100)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
audioContext.onCanplay(trySetDuration)
|
|||
|
|
audioContext.onTimeUpdate(trySetDuration)
|
|||
|
|
audioContext.onError(() => {
|
|||
|
|
try { audioContext.destroy() } catch (e) {}
|
|||
|
|
})
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (!hasGotDuration) {
|
|||
|
|
try { audioContext.destroy() } catch (e) {}
|
|||
|
|
}
|
|||
|
|
}, 3000)
|
|||
|
|
},
|
|||
|
|
// 提交修改
|
|||
|
|
submitEdit() {
|
|||
|
|
if (!this.editForm.fileUrl) {
|
|||
|
|
uni.showToast({ title: '请上传语音文件', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!this.editForm.no) {
|
|||
|
|
uni.showToast({ title: '请选择语音编号', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const areaId = this.editForm.areaId || this.voiceDetail.areaId
|
|||
|
|
if (!areaId) {
|
|||
|
|
uni.showToast({ title: '未获取到运营区信息', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const data = {
|
|||
|
|
id: this.voiceId,
|
|||
|
|
fileUrl: this.editForm.fileUrl,
|
|||
|
|
no: parseInt(this.editForm.no),
|
|||
|
|
areaId: areaId,
|
|||
|
|
remark: this.editForm.remark || ''
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: '保存中...',
|
|||
|
|
mask: true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.$u.post('/bst/voice', data).then((res) => {
|
|||
|
|
if (res.code == 200) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '修改成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 3000
|
|||
|
|
})
|
|||
|
|
setTimeout(()=>{
|
|||
|
|
uni.navigateBack()
|
|||
|
|
},1000)
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: res.msg || '修改失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 5000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}).finally(() => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 删除语音
|
|||
|
|
deleteVoice() {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '提示',
|
|||
|
|
content: '确定要删除这条语音吗?',
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: '删除中...',
|
|||
|
|
mask: true
|
|||
|
|
})
|
|||
|
|
this.$u.delete(`/bst/voice/${this.voiceId}`).then((res) => {
|
|||
|
|
if (res.code == 200) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
setTimeout(() => {
|
|||
|
|
uni.navigateBack()
|
|||
|
|
}, 1500)
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: res.msg || '删除失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}).finally(() => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss">
|
|||
|
|
page {
|
|||
|
|
background: #F7F7F7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 100vh;
|
|||
|
|
padding: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-container {
|
|||
|
|
height: 100vh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-section {
|
|||
|
|
padding: 20rpx;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
margin-bottom: 24rpx;
|
|||
|
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
|
|||
|
|
&::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
top: 28rpx;
|
|||
|
|
bottom: 28rpx;
|
|||
|
|
width: 6rpx;
|
|||
|
|
border-radius: 3rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-item {
|
|||
|
|
padding: 20rpx 0;
|
|||
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|||
|
|
|
|||
|
|
&:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-label {
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
color: #999;
|
|||
|
|
margin-bottom: 12rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-value {
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
color: #262B37;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-url {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12rpx;
|
|||
|
|
color: #3996FD;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-section {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
padding: 20rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 88rpx;
|
|||
|
|
border-radius: 16rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.edit-btn {
|
|||
|
|
background: linear-gradient(135deg, #3996FD 0%, #64B6A7 100%);
|
|||
|
|
color: #fff;
|
|||
|
|
box-shadow: 0 4rpx 12rpx rgba(57, 150, 253, 0.25);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.delete-btn {
|
|||
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
|
|||
|
|
color: #fff;
|
|||
|
|
box-shadow: 0 4rpx 12rpx rgba(255, 68, 68, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-container {
|
|||
|
|
height: 100vh;
|
|||
|
|
padding-bottom: 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-card {
|
|||
|
|
background: #FFFFFF;
|
|||
|
|
border-radius: 24rpx;
|
|||
|
|
padding: 40rpx 32rpx;
|
|||
|
|
margin-bottom: 24rpx;
|
|||
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-item {
|
|||
|
|
.form-label {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 24rpx;
|
|||
|
|
padding-left: 8rpx;
|
|||
|
|
.label-text { font-size: 30rpx; color: #333; font-weight: 600; }
|
|||
|
|
.required { color: #FF4444; margin-left: 8rpx; font-size: 28rpx; }
|
|||
|
|
.label-tip { font-size: 24rpx; color: #909399; margin-left: 8rpx; font-weight: 400; }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-box {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 200rpx;
|
|||
|
|
background: #F5F7FA;
|
|||
|
|
border: 3rpx dashed #D3D4D6;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
.upload-placeholder {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
padding: 40rpx;
|
|||
|
|
.upload-tip { font-size: 28rpx; color: #666; font-weight: 500; }
|
|||
|
|
}
|
|||
|
|
.audio-bar {
|
|||
|
|
width: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
padding: 28rpx 24rpx;
|
|||
|
|
}
|
|||
|
|
.audio-remove, .audio-play-btn { flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
|||
|
|
.audio-play-btn { width: 52rpx; height: 52rpx; margin-right: 50rpx; }
|
|||
|
|
.audio-progress-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
|
|||
|
|
.audio-slider { width: 100%; margin: 0; }
|
|||
|
|
.audio-time { font-size: 22rpx; color: #909399; margin-top: 6rpx; margin-left: 50rpx; }
|
|||
|
|
|
|||
|
|
.picker-box {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 96rpx;
|
|||
|
|
background: #F5F7FA;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 0 24rpx;
|
|||
|
|
.picker-text { font-size: 30rpx; color: #333; font-weight: 500; &.placeholder { color: #C0C4CC; } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-tip {
|
|||
|
|
margin-top: 16rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #909399;
|
|||
|
|
padding-left: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-textarea {
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 160rpx;
|
|||
|
|
padding: 24rpx;
|
|||
|
|
background: #F5F7FA;
|
|||
|
|
border-radius: 20rpx;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #333;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
margin-top: 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-btn {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 88rpx;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cancel-btn {
|
|||
|
|
background: #f0f0f0;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.submit-btn {
|
|||
|
|
background: linear-gradient(135deg, #3996FD 0%, #64B6A7 100%);
|
|||
|
|
color: #fff;
|
|||
|
|
box-shadow: 0 4rpx 12rpx rgba(57, 150, 253, 0.25);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.no-picker-mask {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
z-index: 9998;
|
|||
|
|
}
|
|||
|
|
.no-picker-modal {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 50%; left: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
width: 560rpx;
|
|||
|
|
max-height: 70vh;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 24rpx;
|
|||
|
|
overflow: hidden;
|
|||
|
|
z-index: 9999;
|
|||
|
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
|
|||
|
|
}
|
|||
|
|
.no-picker-title {
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #262B37;
|
|||
|
|
padding: 32rpx;
|
|||
|
|
}
|
|||
|
|
.no-picker-list { max-height: 480rpx; }
|
|||
|
|
.no-picker-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 28rpx 32rpx;
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
color: #333;
|
|||
|
|
border-top: 1px solid #f3f1f1;
|
|||
|
|
&.active { color: #3996FD; font-weight: 600; background: #EFF6FF; }
|
|||
|
|
&.disabled { color: #B0B0B0; background: #F7F7F7; }
|
|||
|
|
}
|
|||
|
|
.no-picker-right { display: flex; align-items: center; }
|
|||
|
|
.configured-tag {
|
|||
|
|
font-size: 22rpx;
|
|||
|
|
color: #999;
|
|||
|
|
padding: 6rpx 12rpx;
|
|||
|
|
background: #EFEFEF;
|
|||
|
|
border-radius: 999rpx;
|
|||
|
|
}
|
|||
|
|
.no-picker-cancel {
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
color: #999;
|
|||
|
|
padding: 24rpx;
|
|||
|
|
border-top: 1rpx solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
</style>
|