chuangte_bike_newxcx/page_shanghu/gongzuotai/yuyin/add.vue

877 lines
20 KiB
Vue
Raw Permalink Normal View History

2026-02-06 15:28:01 +08:00
<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="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="!fileUrl && uploadFile()">
<view v-if="!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', selectedNo ? '' : 'placeholder']">
{{ selectedNo || '请选择语音编号(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="remark"
placeholder="请输入备注信息"
maxlength="200"
:auto-height="true"
></textarea>
</view>
</view>
</scroll-view>
<view class="submit-btn" @click="submit">
<text>提交</text>
</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: selectedNo === 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="selectedNo === 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",
},
fileUrl: '',
fileName: '',
token: '',
upurl: '',
noOptions: [],
noIndex: -1,
selectedNo: '',
areaId: '',
remark: '',
showNoPicker: false,
configuredNoSet: {}, // 已配置编号集合key 为 label 字符串)
// 语音预览
previewAudio: null,
previewPlaying: false,
previewProgress: 0,
previewCurrentTime: 0,
previewDuration: 0,
previewSeekChanging: false
}
},
onUnload() {
this.destroyPreviewAudio()
},
onLoad() {
this.initNoOptions()
this.getUserAreaId()
this.getQiniuToken()
},
methods: {
// 初始化编号选项 11-40
initNoOptions() {
for (let i = 11; i <= 40; i++) {
this.noOptions.push({
value: i,
label: i.toString()
})
}
},
// 获取用户运营区ID
getUserAreaId() {
// 优先从存储中获取
if (uni.getStorageSync('adminAreaid')) {
this.areaId = uni.getStorageSync('adminAreaid')
this.loadConfiguredNos()
return
}
// 从用户信息中获取
this.$u.get("/getInfo").then(res => {
if (res.code == 200 && res.user && res.user.areaId) {
this.areaId = res.user.areaId
this.loadConfiguredNos()
}
})
},
// 获取七牛云token
getQiniuToken() {
this.$u.get("/common/qiniuToken").then((res) => {
if (res.code == 200) {
this.token = res.data
this.upurl = res.domain
}
})
},
// 拉取当前运营区已配置的语音编号,用于弹窗置灰
loadConfiguredNos() {
if (!this.areaId) return
this.$u.get('/bst/voice/list?areaId=' + this.areaId).then(res => {
if (res.code != 200) return
const rows = Array.isArray(res.data) ? res.data : (res.rows || [])
const set = {}
rows.forEach(item => {
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.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.selectedNo = this.noOptions[index].label
this.showNoPicker = false
},
// 上传文件
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'
// #ifdef H5
// 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.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
// #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.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
},
// 移除文件
removeFile() {
this.destroyPreviewAudio()
this.fileUrl = ''
this.fileName = ''
},
// 语音预览:播放/暂停
togglePreviewPlay() {
if (!this.fileUrl) return
if (this.previewPlaying) {
this.pausePreview()
} else {
this.playPreview()
}
},
playPreview() {
if (!this.fileUrl) return
if (this.previewAudio) {
this.previewAudio.play()
this.previewPlaying = true
return
}
const ctx = uni.createInnerAudioContext()
ctx.src = this.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(e) {
this.previewSeekChanging = true
},
onPreviewSeek(e) {
const p = Number(e.detail.value) || 0
this.previewSeekChanging = false
if (!this.previewAudio || !this.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
},
// 上传成功后预加载音频以获取时长,便于下方显示
preloadPreviewDuration() {
if (!this.fileUrl) return
const ctx = uni.createInnerAudioContext()
ctx.src = this.fileUrl
ctx.autoplay = false
let done = false
const setDuration = () => {
if (done || !ctx.duration || ctx.duration <= 0) return
done = true
this.previewDuration = ctx.duration
this.$forceUpdate()
setTimeout(() => {
ctx.destroy()
}, 100)
}
ctx.onCanplay(setDuration)
ctx.onTimeUpdate(() => setDuration())
ctx.onError(() => {
if (!done) ctx.destroy()
})
setTimeout(() => {
if (!done) {
done = true
ctx.destroy()
}
}, 5000)
},
// 提交
submit() {
if (!this.fileUrl) {
uni.showToast({
title: '请上传语音文件',
icon: 'none',
duration: 2000
})
return
}
if (!this.selectedNo) {
uni.showToast({
title: '请选择语音编号',
icon: 'none',
duration: 2000
})
return
}
if (!this.areaId) {
uni.showToast({
title: '未获取到运营区信息',
icon: 'none',
duration: 2000
})
return
}
const data = {
fileUrl: this.fileUrl,
no: parseInt(this.selectedNo),
areaId: this.areaId,
remark: this.remark || ''
}
uni.showLoading({
title: '提交中...',
mask: true
})
this.$u.post('/bst/voice', data).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
})
}
}).catch(err => {
uni.showToast({
title: '提交失败',
icon: 'none',
duration: 2000
})
}).finally(() => {
uni.hideLoading()
})
}
}
}
</script>
<style lang="scss">
page {
background: #F7F7F7;
}
.page {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 0 20rpx;
}
.form-container {
flex: 1;
padding-bottom: 140rpx;
}
.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;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
width: 6rpx;
height: 28rpx;
border-radius: 3rpx;
}
.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;
transition: all 0.3s ease;
&:active {
border-color: #3996FD;
background: #EFF6FF;
}
}
.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 {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
}
.audio-play-btn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
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;
display: flex;
align-items: center;
gap: 8rpx;
margin-left: 50rpx;
}
.form-tip {
margin-top: 16rpx;
font-size: 24rpx;
color: #909399;
padding-left: 8rpx;
}
.picker-box {
width: 100%;
height: 96rpx;
background: #F5F7FA;
border: 2rpx solid transparent;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
transition: all 0.3s;
&:active {
background: #FFFFFF;
border-color: #3996FD;
box-shadow: 0 0 0 4rpx rgba(57, 150, 253, 0.12);
}
.picker-text {
font-size: 30rpx;
color: #333;
font-weight: 500;
&.placeholder {
color: #C0C4CC;
}
}
}
.form-textarea {
width: 100%;
min-height: 160rpx;
padding: 24rpx;
background: #F5F7FA;
border: 2rpx solid transparent;
border-radius: 20rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
transition: all 0.3s;
&:focus {
background: #FFFFFF;
border-color: #3996FD;
box-shadow: 0 0 0 4rpx rgba(57, 150, 253, 0.12);
}
}
.submit-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: linear-gradient(135deg, #3996FD 0%, #64B6A7 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
font-weight: 600;
z-index: 100;
box-shadow: 0 -4rpx 20rpx rgba(57, 150, 253, 0.25);
&::after {
border: none;
}
&:active {
opacity: 0.9;
}
}
/* 语音编号选择弹窗:居中、可滚动、点击选择 */
.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>