分段压缩上传
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) {
|
||||
@@ -1531,6 +1676,33 @@ export default {
|
||||
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 {
|
||||
position: fixed;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user