分段压缩上传
This commit is contained in:
@@ -127,7 +127,7 @@
|
||||
<text>补录客户</text>
|
||||
</view>
|
||||
<view class="service-action-btn service-action-btn--upload" @click.stop="chooseAndUploadFile(item)">
|
||||
<text>{{ isUploading(item.id) ? '上传中' : '上传录音' }}</text>
|
||||
<text>上传录音</text>
|
||||
</view>
|
||||
<view class="service-action-btn service-action-btn--finish" @click.stop="finishService(item)">
|
||||
<text>结束服务</text>
|
||||
@@ -137,6 +137,18 @@
|
||||
<text>已录音:{{ formatRecordingElapsed(recordingElapsedSeconds) }}</text>
|
||||
<text>当前大小:{{ formatRecordingSize(recordingEstimatedSizeBytes) }}</text>
|
||||
</view>
|
||||
<view v-if="getUploadResultList(item.id).length" class="upload-result-list">
|
||||
<view
|
||||
v-for="segment in getUploadResultList(item.id)"
|
||||
:key="segment.id"
|
||||
class="upload-result-item"
|
||||
>
|
||||
<text class="upload-result-item__main">
|
||||
第{{ segment.segmentNo }}段({{ segment.durationText || '--:--' }} / {{ segment.sizeText || '--' }}):{{ segment.statusText }}
|
||||
</text>
|
||||
<text v-if="segment.message" class="upload-result-item__sub">{{ segment.message }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="service-status-empty" v-if="!serviceStatusLoading && !serviceStatusList.length">
|
||||
<text>{{ emptyListHint }}</text>
|
||||
@@ -276,7 +288,17 @@ export default {
|
||||
recordingElapsedSeconds: 0,
|
||||
recordingEstimatedSizeBytes: 0,
|
||||
recordingTicker: null,
|
||||
keepScreenOnEnabled: false
|
||||
keepScreenOnEnabled: false,
|
||||
// 上传前压缩策略:从录音源头降低采样率/码率,减少上传文件体积
|
||||
recordingSampleRate: 16000,
|
||||
recordingEncodeBitRate: 32000,
|
||||
recordingSegmentDurationMs: 5 * 60 * 1000,
|
||||
recordingManualStopRequested: false,
|
||||
recordingCurrentSegmentNo: 0,
|
||||
recordingCurrentSegmentStartAt: 0,
|
||||
recordingCurrentSegmentElapsedSeconds: 0,
|
||||
uploadingCountMap: {},
|
||||
uploadResultMap: {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -305,27 +327,71 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.recorderManager = uni.getRecorderManager();
|
||||
// 用户点击「停止」后由系统回调此处,再将临时录音文件上传到 /api/audio/upload
|
||||
// 录音每段停止后回调:静默上传,若非手动停止则继续下一段
|
||||
this.recorderManager.onStop((res) => {
|
||||
const ctx = this.recordingContext;
|
||||
const recordId = this.recordingServiceId;
|
||||
const currentSegmentNo = this.recordingCurrentSegmentNo;
|
||||
const currentSegmentElapsedSeconds = this.recordingCurrentSegmentElapsedSeconds || this.recordingElapsedSeconds || 0;
|
||||
const shouldContinue =
|
||||
!!recordId &&
|
||||
!!ctx &&
|
||||
!!ctx.id &&
|
||||
!this.recordingManualStopRequested;
|
||||
const normalizedSegmentNo = currentSegmentNo > 0 ? currentSegmentNo : 1;
|
||||
this.stopRecordingTicker();
|
||||
const tempFilePath = res.tempFilePath || '';
|
||||
if (ctx && ctx.id) {
|
||||
const name = `service_record_${ctx.id}_${Date.now()}_part${normalizedSegmentNo}.mp3`;
|
||||
if (!tempFilePath) {
|
||||
this.pushUploadResult(String(ctx.id), {
|
||||
id: `seg_${String(ctx.id)}_${normalizedSegmentNo}_${Date.now()}`,
|
||||
segmentNo: normalizedSegmentNo,
|
||||
durationText: this.formatRecordingElapsed(currentSegmentElapsedSeconds),
|
||||
sizeText: '--',
|
||||
status: 'failed',
|
||||
statusText: '上传失败',
|
||||
message: '未获取到录音文件(已重试1次)',
|
||||
});
|
||||
} else {
|
||||
this.uploadFileForRecord(
|
||||
ctx,
|
||||
{
|
||||
path: tempFilePath,
|
||||
name,
|
||||
size: Number(res.fileSize) || 0,
|
||||
},
|
||||
{
|
||||
silent: true,
|
||||
segmentNo: normalizedSegmentNo,
|
||||
durationSeconds: currentSegmentElapsedSeconds,
|
||||
retryOnceOnFail: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
if (shouldContinue) {
|
||||
this.setKeepScreenOn(true);
|
||||
setTimeout(() => {
|
||||
this.startNewSegmentRecording();
|
||||
}, 80);
|
||||
return;
|
||||
}
|
||||
this.recordingServiceId = null;
|
||||
this.recordingContext = null;
|
||||
this.stopRecordingTicker();
|
||||
this.recordingManualStopRequested = false;
|
||||
this.recordingCurrentSegmentNo = 0;
|
||||
this.recordingCurrentSegmentStartAt = 0;
|
||||
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||
this.setKeepScreenOn(false);
|
||||
const tempFilePath = res.tempFilePath || '';
|
||||
if (!ctx || !ctx.id) {
|
||||
return;
|
||||
}
|
||||
if (!tempFilePath) {
|
||||
uni.showToast({ title: '未获取到录音文件', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const name = `service_record_${ctx.id}_${Date.now()}.mp3`;
|
||||
this.uploadFileForRecord(ctx, { path: tempFilePath, name });
|
||||
});
|
||||
this.recorderManager.onError(() => {
|
||||
this.recordingServiceId = null;
|
||||
this.recordingContext = null;
|
||||
this.recordingManualStopRequested = false;
|
||||
this.recordingCurrentSegmentNo = 0;
|
||||
this.recordingCurrentSegmentStartAt = 0;
|
||||
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||
this.stopRecordingTicker();
|
||||
this.setKeepScreenOn(false);
|
||||
uni.showToast({ title: '录音出错', icon: 'none' });
|
||||
@@ -349,10 +415,10 @@ export default {
|
||||
this.stopRecordingTicker();
|
||||
this.recordingElapsedSeconds = 0;
|
||||
this.recordingEstimatedSizeBytes = 0;
|
||||
// 96kbps(encodeBitRate)约等于每秒 12000 字节
|
||||
const bytesPerSecond = 96000 / 8;
|
||||
const bytesPerSecond = (Number(this.recordingEncodeBitRate) || 32000) / 8;
|
||||
this.recordingTicker = setInterval(() => {
|
||||
this.recordingElapsedSeconds += 1;
|
||||
this.recordingCurrentSegmentElapsedSeconds = this.recordingElapsedSeconds;
|
||||
this.recordingEstimatedSizeBytes = Math.floor(this.recordingElapsedSeconds * bytesPerSecond);
|
||||
}, 1000);
|
||||
},
|
||||
@@ -361,9 +427,38 @@ export default {
|
||||
clearInterval(this.recordingTicker);
|
||||
}
|
||||
this.recordingTicker = null;
|
||||
this.recordingCurrentSegmentElapsedSeconds = this.recordingElapsedSeconds;
|
||||
this.recordingElapsedSeconds = 0;
|
||||
this.recordingEstimatedSizeBytes = 0;
|
||||
},
|
||||
startNewSegmentRecording() {
|
||||
if (!this.recorderManager || !this.recordingServiceId || !this.recordingContext?.id) {
|
||||
return;
|
||||
}
|
||||
this.recordingCurrentSegmentNo += 1;
|
||||
this.recordingCurrentSegmentStartAt = Date.now();
|
||||
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||
try {
|
||||
this.recorderManager.start({
|
||||
duration: this.recordingSegmentDurationMs,
|
||||
sampleRate: this.recordingSampleRate,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: this.recordingEncodeBitRate,
|
||||
format: 'mp3'
|
||||
});
|
||||
this.startRecordingTicker();
|
||||
} catch (e) {
|
||||
this.recordingServiceId = null;
|
||||
this.recordingContext = null;
|
||||
this.recordingManualStopRequested = false;
|
||||
this.recordingCurrentSegmentNo = 0;
|
||||
this.recordingCurrentSegmentStartAt = 0;
|
||||
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||
this.stopRecordingTicker();
|
||||
this.setKeepScreenOn(false);
|
||||
uni.showToast({ title: '无法开始录音', icon: 'none' });
|
||||
}
|
||||
},
|
||||
formatRecordingElapsed(seconds) {
|
||||
const safeSeconds = Math.max(0, Number(seconds) || 0);
|
||||
const minutes = String(Math.floor(safeSeconds / 60)).padStart(2, '0');
|
||||
@@ -409,25 +504,15 @@ export default {
|
||||
uni.showToast({ title: '正在上传,请稍候', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.recordingManualStopRequested = false;
|
||||
this.recordingCurrentSegmentNo = 0;
|
||||
this.recordingCurrentSegmentStartAt = 0;
|
||||
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||
this.setUploadResultList(String(item.id), []);
|
||||
this.recordingContext = { ...item };
|
||||
this.recordingServiceId = String(item.id);
|
||||
try {
|
||||
this.recorderManager.start({
|
||||
duration: 600000,
|
||||
sampleRate: 44100,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 96000,
|
||||
format: 'mp3'
|
||||
});
|
||||
this.setKeepScreenOn(true);
|
||||
this.startRecordingTicker();
|
||||
} catch (e) {
|
||||
this.recordingServiceId = null;
|
||||
this.recordingContext = null;
|
||||
this.stopRecordingTicker();
|
||||
this.setKeepScreenOn(false);
|
||||
uni.showToast({ title: '无法开始录音', icon: 'none' });
|
||||
}
|
||||
this.setKeepScreenOn(true);
|
||||
this.startNewSegmentRecording();
|
||||
},
|
||||
/** 结束录音;onStop 回调中会调用 uploadFileForRecord 上传至后端 */
|
||||
stopPhoneRecordAndUpload(item) {
|
||||
@@ -437,8 +522,7 @@ export default {
|
||||
if (String(item.id) !== this.recordingServiceId) {
|
||||
return;
|
||||
}
|
||||
this.stopRecordingTicker();
|
||||
this.setKeepScreenOn(false);
|
||||
this.recordingManualStopRequested = true;
|
||||
this.recorderManager.stop();
|
||||
},
|
||||
/**
|
||||
@@ -698,7 +782,41 @@ export default {
|
||||
},
|
||||
isUploading(recordId) {
|
||||
if (!recordId) return false;
|
||||
return this.uploadingIds.includes(String(recordId));
|
||||
return Number(this.uploadingCountMap[String(recordId)] || 0) > 0;
|
||||
},
|
||||
increaseUploadingCount(recordId) {
|
||||
const key = String(recordId);
|
||||
const next = Number(this.uploadingCountMap[key] || 0) + 1;
|
||||
this.uploadingCountMap = { ...this.uploadingCountMap, [key]: next };
|
||||
},
|
||||
decreaseUploadingCount(recordId) {
|
||||
const key = String(recordId);
|
||||
const current = Number(this.uploadingCountMap[key] || 0);
|
||||
const next = Math.max(0, current - 1);
|
||||
this.uploadingCountMap = { ...this.uploadingCountMap, [key]: next };
|
||||
},
|
||||
getUploadResultList(recordId) {
|
||||
const key = String(recordId || '');
|
||||
const list = this.uploadResultMap[key];
|
||||
return Array.isArray(list) ? list : [];
|
||||
},
|
||||
setUploadResultList(recordId, list) {
|
||||
const key = String(recordId || '');
|
||||
this.uploadResultMap = { ...this.uploadResultMap, [key]: Array.isArray(list) ? list : [] };
|
||||
},
|
||||
pushUploadResult(recordId, segment) {
|
||||
const list = this.getUploadResultList(recordId);
|
||||
this.setUploadResultList(recordId, [...list, segment]);
|
||||
},
|
||||
updateUploadResult(recordId, segmentResultId, patch = {}) {
|
||||
const list = this.getUploadResultList(recordId);
|
||||
const nextList = list.map((it) => {
|
||||
if (it.id !== segmentResultId) {
|
||||
return it;
|
||||
}
|
||||
return { ...it, ...patch };
|
||||
});
|
||||
this.setUploadResultList(recordId, nextList);
|
||||
},
|
||||
/** 仅允许扩展名为 .mp3 的文件(不区分大小写) */
|
||||
isMp3UploadFile(selected) {
|
||||
@@ -823,55 +941,82 @@ export default {
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
async uploadFileForRecord(item, selectedFile) {
|
||||
async uploadFileForRecord(item, selectedFile, options = {}) {
|
||||
const recordId = String(item.id);
|
||||
this.uploadingIds = [...this.uploadingIds, recordId];
|
||||
const segmentNo = Number(options.segmentNo) || 1;
|
||||
const durationSeconds = Math.max(0, Number(options.durationSeconds) || 0);
|
||||
const fileSize = Math.max(0, Number(selectedFile?.size) || 0);
|
||||
const resultId = `seg_${recordId}_${segmentNo}_${Date.now()}`;
|
||||
this.pushUploadResult(recordId, {
|
||||
id: resultId,
|
||||
segmentNo,
|
||||
durationText: this.formatRecordingElapsed(durationSeconds),
|
||||
sizeText: this.formatRecordingSize(fileSize),
|
||||
status: 'uploading',
|
||||
statusText: '上传中',
|
||||
message: '',
|
||||
});
|
||||
this.increaseUploadingCount(recordId);
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '上传中...'
|
||||
});
|
||||
const uploadRes = await uni.uploadFile({
|
||||
url: getApiUrl('/api/audio/upload'),
|
||||
filePath: selectedFile.path,
|
||||
name: 'file',
|
||||
formData: {
|
||||
id: item.id,
|
||||
audioId: item.id,
|
||||
customerId: item.customerId || '',
|
||||
customerName: item.customerName || '',
|
||||
fileName: selectedFile.name || `service_file_${Date.now()}`
|
||||
},
|
||||
header: this.getAuthHeaders(),
|
||||
timeout: 60000
|
||||
});
|
||||
let result = {};
|
||||
try {
|
||||
result = typeof uploadRes?.data === 'string' ? JSON.parse(uploadRes.data) : (uploadRes?.data || {});
|
||||
} catch (e) {
|
||||
result = uploadRes?.data || {};
|
||||
}
|
||||
if (uploadRes?.statusCode === 200 && result?.success !== false) {
|
||||
uni.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success'
|
||||
const maxAttempt = options.retryOnceOnFail ? 2 : 1;
|
||||
let lastErrorMessage = '';
|
||||
for (let attempt = 1; attempt <= maxAttempt; attempt += 1) {
|
||||
const uploadRes = await uni.uploadFile({
|
||||
url: getApiUrl('/api/audio/upload'),
|
||||
filePath: selectedFile.path,
|
||||
name: 'file',
|
||||
formData: {
|
||||
id: item.id,
|
||||
audioId: item.id,
|
||||
customerId: item.customerId || '',
|
||||
customerName: item.customerName || '',
|
||||
fileName: selectedFile.name || `service_file_${Date.now()}`,
|
||||
compress: true
|
||||
},
|
||||
header: this.getAuthHeaders(),
|
||||
timeout: 60000
|
||||
});
|
||||
this.serviceStatusPage.current = 1;
|
||||
this.fetchServiceStatusList({ force: true });
|
||||
return;
|
||||
let result = {};
|
||||
try {
|
||||
result = typeof uploadRes?.data === 'string' ? JSON.parse(uploadRes.data) : (uploadRes?.data || {});
|
||||
} catch (e) {
|
||||
result = uploadRes?.data || {};
|
||||
}
|
||||
if (uploadRes?.statusCode === 200 && result?.success !== false) {
|
||||
this.updateUploadResult(recordId, resultId, {
|
||||
status: 'success',
|
||||
statusText: '上传成功',
|
||||
message: attempt > 1 ? `第${attempt}次尝试成功` : '',
|
||||
});
|
||||
this.serviceStatusPage.current = 1;
|
||||
this.fetchServiceStatusList({ force: true });
|
||||
return true;
|
||||
}
|
||||
lastErrorMessage = result?.message || `上传失败(HTTP ${uploadRes?.statusCode || '--'})`;
|
||||
if (attempt < maxAttempt) {
|
||||
this.updateUploadResult(recordId, resultId, {
|
||||
status: 'retrying',
|
||||
statusText: '重试中',
|
||||
message: `${lastErrorMessage},正在重试`,
|
||||
});
|
||||
}
|
||||
}
|
||||
uni.showToast({
|
||||
title: result?.message || '上传失败',
|
||||
icon: 'none'
|
||||
this.updateUploadResult(recordId, resultId, {
|
||||
status: 'failed',
|
||||
statusText: '上传失败',
|
||||
message: `${lastErrorMessage || '未知错误'}(已重试1次)`,
|
||||
});
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
uni.showToast({
|
||||
title: error?.errMsg || error?.message || '上传失败',
|
||||
icon: 'none'
|
||||
this.updateUploadResult(recordId, resultId, {
|
||||
status: 'failed',
|
||||
statusText: '上传失败',
|
||||
message: `${error?.errMsg || error?.message || '未知错误'}(已重试1次)`,
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
this.uploadingIds = this.uploadingIds.filter(id => id !== recordId);
|
||||
this.decreaseUploadingCount(recordId);
|
||||
}
|
||||
},
|
||||
async finishService(item) {
|
||||
@@ -1530,6 +1675,33 @@ export default {
|
||||
font-size: 24rpx;
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.upload-result-list {
|
||||
margin-top: 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background-color: #F8FAFF;
|
||||
border-radius: 10rpx;
|
||||
border: 1px solid #E7EEFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.upload-result-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.upload-result-item__main {
|
||||
font-size: 24rpx;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.upload-result-item__sub {
|
||||
font-size: 22rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
/* 补录弹窗样式 */
|
||||
.supplement-dialog-mask {
|
||||
|
||||
Reference in New Issue
Block a user