Compare commits
3 Commits
f373f591dd
...
87afece84f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87afece84f | ||
|
|
e7976c0993 | ||
|
|
811f5ad16b |
@@ -62,46 +62,9 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="staff-name">{{ item.staffName || '未分配销售' }}</text>
|
<text class="staff-name">{{ item.staffName || '未分配销售' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-status">
|
<view class="service-status-indicator">
|
||||||
<view class="action-trigger">
|
<uni-icons type="bars" size="16" color="#007AFF"></uni-icons>
|
||||||
<view class="action-trigger__primary" @click.stop="finishService(item)">
|
<text class="service-status-indicator__text">{{ recordStateLabel }}</text>
|
||||||
<text class="action-trigger__primary-text">结束服务</text>
|
|
||||||
</view>
|
|
||||||
<view class="action-trigger__more" @click.stop="toggleActionMenu(item.id)">
|
|
||||||
<uni-icons type="bottom" size="16" color="#007AFF"></uni-icons>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view
|
|
||||||
v-if="actionMenuForId && String(actionMenuForId) === String(item.id)"
|
|
||||||
class="action-menu"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="action-menu-item"
|
|
||||||
@click.stop="onActionMenuSelect('record', item)"
|
|
||||||
>
|
|
||||||
<text>{{ isRecordingItem(item.id) ? '停止录音' : '开始录音' }}</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="action-menu-item"
|
|
||||||
@click.stop="onActionMenuSelect('supplement', item)"
|
|
||||||
>
|
|
||||||
<text>补录客户</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="action-menu-item"
|
|
||||||
:class="{ 'action-menu-item--disabled': isUploading(item.id) }"
|
|
||||||
@click.stop="onActionMenuSelect('upload', item)"
|
|
||||||
>
|
|
||||||
<text>{{ isUploading(item.id) ? '上传中' : '上传录音' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<template v-if="showRecordStateIndicator">
|
|
||||||
<uni-icons type="bars" size="16" color="#007AFF"></uni-icons>
|
|
||||||
<text>{{ recordStateLabel }}</text>
|
|
||||||
</template>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -145,6 +108,47 @@
|
|||||||
<view class="service-duration">
|
<view class="service-duration">
|
||||||
<text>{{ item.durationText }}</text>
|
<text>{{ item.durationText }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="service-actions-row">
|
||||||
|
<view
|
||||||
|
v-if="!isRecordingItem(item.id)"
|
||||||
|
class="service-action-btn service-action-btn--record"
|
||||||
|
@click.stop="startPhoneRecord(item)"
|
||||||
|
>
|
||||||
|
<text>录音</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="service-action-btn service-action-btn--recording"
|
||||||
|
@click.stop="stopPhoneRecordAndUpload(item)"
|
||||||
|
>
|
||||||
|
<text>停止录音</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-action-btn service-action-btn--supplement" @click.stop="showSupplementDialog(item)">
|
||||||
|
<text>补录客户</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-action-btn service-action-btn--upload" @click.stop="chooseAndUploadFile(item)">
|
||||||
|
<text>上传录音</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-action-btn service-action-btn--finish" @click.stop="finishService(item)">
|
||||||
|
<text>结束服务</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="isRecordingItem(item.id)" class="recording-realtime">
|
||||||
|
<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>
|
||||||
<view class="service-status-empty" v-if="!serviceStatusLoading && !serviceStatusList.length">
|
<view class="service-status-empty" v-if="!serviceStatusLoading && !serviceStatusList.length">
|
||||||
<text>{{ emptyListHint }}</text>
|
<text>{{ emptyListHint }}</text>
|
||||||
@@ -154,12 +158,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<view
|
|
||||||
v-if="actionMenuForId"
|
|
||||||
class="action-menu-mask"
|
|
||||||
@click="closeActionMenu"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 补录弹窗 -->
|
<!-- 补录弹窗 -->
|
||||||
<view class="supplement-dialog-mask" v-if="showSupplementModal" @click="closeSupplementDialog">
|
<view class="supplement-dialog-mask" v-if="showSupplementModal" @click="closeSupplementDialog">
|
||||||
<view class="supplement-dialog" @click.stop>
|
<view class="supplement-dialog" @click.stop>
|
||||||
@@ -287,7 +285,20 @@ export default {
|
|||||||
recorderManager: null,
|
recorderManager: null,
|
||||||
recordingServiceId: null,
|
recordingServiceId: null,
|
||||||
recordingContext: null,
|
recordingContext: null,
|
||||||
actionMenuForId: null
|
recordingElapsedSeconds: 0,
|
||||||
|
recordingEstimatedSizeBytes: 0,
|
||||||
|
recordingTicker: null,
|
||||||
|
keepScreenOnEnabled: false,
|
||||||
|
// 上传前压缩策略:从录音源头降低采样率/码率,减少上传文件体积
|
||||||
|
recordingSampleRate: 16000,
|
||||||
|
recordingEncodeBitRate: 32000,
|
||||||
|
recordingSegmentDurationMs: 5 * 60 * 1000,
|
||||||
|
recordingManualStopRequested: false,
|
||||||
|
recordingCurrentSegmentNo: 0,
|
||||||
|
recordingCurrentSegmentStartAt: 0,
|
||||||
|
recordingCurrentSegmentElapsedSeconds: 0,
|
||||||
|
uploadingCountMap: {},
|
||||||
|
uploadResultMap: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -299,6 +310,8 @@ export default {
|
|||||||
this.fetchServiceStatusList();
|
this.fetchServiceStatusList();
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
|
this.stopRecordingTicker();
|
||||||
|
this.setKeepScreenOn(false);
|
||||||
if (this.recorderManager && this.recordingServiceId) {
|
if (this.recorderManager && this.recordingServiceId) {
|
||||||
try {
|
try {
|
||||||
this.recorderManager.stop();
|
this.recorderManager.stop();
|
||||||
@@ -308,79 +321,160 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleActionMenu(recordId) {
|
|
||||||
const id = recordId === undefined || recordId === null ? null : String(recordId);
|
|
||||||
if (!id) {
|
|
||||||
this.actionMenuForId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.actionMenuForId = this.actionMenuForId === id ? null : id;
|
|
||||||
},
|
|
||||||
closeActionMenu() {
|
|
||||||
this.actionMenuForId = null;
|
|
||||||
},
|
|
||||||
onActionMenuSelect(action, item) {
|
|
||||||
if (!item || !item.id) {
|
|
||||||
this.closeActionMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = String(item.id);
|
|
||||||
if (this.actionMenuForId && String(this.actionMenuForId) !== id) {
|
|
||||||
// 避免列表复用导致错点
|
|
||||||
this.actionMenuForId = id;
|
|
||||||
}
|
|
||||||
if (action === 'record') {
|
|
||||||
if (this.isRecordingItem(item.id)) {
|
|
||||||
this.stopPhoneRecordAndUpload(item);
|
|
||||||
} else {
|
|
||||||
this.startPhoneRecord(item);
|
|
||||||
}
|
|
||||||
this.closeActionMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === 'supplement') {
|
|
||||||
this.showSupplementDialog(item);
|
|
||||||
this.closeActionMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === 'upload') {
|
|
||||||
if (this.isUploading(item.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.chooseAndUploadFile(item);
|
|
||||||
this.closeActionMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.closeActionMenu();
|
|
||||||
},
|
|
||||||
initServiceRecorder() {
|
initServiceRecorder() {
|
||||||
this.recorderSupported = typeof uni.getRecorderManager === 'function';
|
this.recorderSupported = typeof uni.getRecorderManager === 'function';
|
||||||
if (!this.recorderSupported) {
|
if (!this.recorderSupported) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.recorderManager = uni.getRecorderManager();
|
this.recorderManager = uni.getRecorderManager();
|
||||||
// 用户点击「停止」后由系统回调此处,再将临时录音文件上传到 /api/audio/upload
|
// 录音每段停止后回调:静默上传,若非手动停止则继续下一段
|
||||||
this.recorderManager.onStop((res) => {
|
this.recorderManager.onStop((res) => {
|
||||||
const ctx = this.recordingContext;
|
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.recordingServiceId = null;
|
||||||
this.recordingContext = null;
|
this.recordingContext = null;
|
||||||
const tempFilePath = res.tempFilePath || '';
|
this.recordingManualStopRequested = false;
|
||||||
if (!ctx || !ctx.id) {
|
this.recordingCurrentSegmentNo = 0;
|
||||||
return;
|
this.recordingCurrentSegmentStartAt = 0;
|
||||||
}
|
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||||
if (!tempFilePath) {
|
this.setKeepScreenOn(false);
|
||||||
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.recorderManager.onError(() => {
|
||||||
this.recordingServiceId = null;
|
this.recordingServiceId = null;
|
||||||
this.recordingContext = 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' });
|
uni.showToast({ title: '录音出错', icon: 'none' });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setKeepScreenOn(enabled) {
|
||||||
|
if (typeof uni.setKeepScreenOn !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.setKeepScreenOn({
|
||||||
|
keepScreenOn: !!enabled,
|
||||||
|
success: () => {
|
||||||
|
this.keepScreenOnEnabled = !!enabled;
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
this.keepScreenOnEnabled = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
startRecordingTicker() {
|
||||||
|
this.stopRecordingTicker();
|
||||||
|
this.recordingElapsedSeconds = 0;
|
||||||
|
this.recordingEstimatedSizeBytes = 0;
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
stopRecordingTicker() {
|
||||||
|
if (this.recordingTicker) {
|
||||||
|
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');
|
||||||
|
const remainSeconds = String(safeSeconds % 60).padStart(2, '0');
|
||||||
|
return `${minutes}:${remainSeconds}`;
|
||||||
|
},
|
||||||
|
formatRecordingSize(bytes) {
|
||||||
|
const size = Math.max(0, Number(bytes) || 0);
|
||||||
|
if (size < 1024) {
|
||||||
|
return `${size} B`;
|
||||||
|
}
|
||||||
|
if (size < 1024 * 1024) {
|
||||||
|
return `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
}
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
||||||
|
},
|
||||||
isRecordingItem(recordId) {
|
isRecordingItem(recordId) {
|
||||||
if (!recordId || !this.recordingServiceId) {
|
if (!recordId || !this.recordingServiceId) {
|
||||||
return false;
|
return false;
|
||||||
@@ -410,21 +504,15 @@ export default {
|
|||||||
uni.showToast({ title: '正在上传,请稍候', icon: 'none' });
|
uni.showToast({ title: '正在上传,请稍候', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.recordingManualStopRequested = false;
|
||||||
|
this.recordingCurrentSegmentNo = 0;
|
||||||
|
this.recordingCurrentSegmentStartAt = 0;
|
||||||
|
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||||
|
this.setUploadResultList(String(item.id), []);
|
||||||
this.recordingContext = { ...item };
|
this.recordingContext = { ...item };
|
||||||
this.recordingServiceId = String(item.id);
|
this.recordingServiceId = String(item.id);
|
||||||
try {
|
this.setKeepScreenOn(true);
|
||||||
this.recorderManager.start({
|
this.startNewSegmentRecording();
|
||||||
duration: 600000,
|
|
||||||
sampleRate: 44100,
|
|
||||||
numberOfChannels: 1,
|
|
||||||
encodeBitRate: 96000,
|
|
||||||
format: 'mp3'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this.recordingServiceId = null;
|
|
||||||
this.recordingContext = null;
|
|
||||||
uni.showToast({ title: '无法开始录音', icon: 'none' });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
/** 结束录音;onStop 回调中会调用 uploadFileForRecord 上传至后端 */
|
/** 结束录音;onStop 回调中会调用 uploadFileForRecord 上传至后端 */
|
||||||
stopPhoneRecordAndUpload(item) {
|
stopPhoneRecordAndUpload(item) {
|
||||||
@@ -434,6 +522,7 @@ export default {
|
|||||||
if (String(item.id) !== this.recordingServiceId) {
|
if (String(item.id) !== this.recordingServiceId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.recordingManualStopRequested = true;
|
||||||
this.recorderManager.stop();
|
this.recorderManager.stop();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -693,7 +782,41 @@ export default {
|
|||||||
},
|
},
|
||||||
isUploading(recordId) {
|
isUploading(recordId) {
|
||||||
if (!recordId) return false;
|
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 的文件(不区分大小写) */
|
/** 仅允许扩展名为 .mp3 的文件(不区分大小写) */
|
||||||
isMp3UploadFile(selected) {
|
isMp3UploadFile(selected) {
|
||||||
@@ -818,56 +941,82 @@ export default {
|
|||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
},
|
},
|
||||||
async uploadFileForRecord(item, selectedFile) {
|
async uploadFileForRecord(item, selectedFile, options = {}) {
|
||||||
const recordId = String(item.id);
|
const recordId = String(item.id);
|
||||||
this.closeActionMenu();
|
const segmentNo = Number(options.segmentNo) || 1;
|
||||||
this.uploadingIds = [...this.uploadingIds, recordId];
|
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 {
|
try {
|
||||||
uni.showLoading({
|
const maxAttempt = options.retryOnceOnFail ? 2 : 1;
|
||||||
title: '上传中...'
|
let lastErrorMessage = '';
|
||||||
});
|
for (let attempt = 1; attempt <= maxAttempt; attempt += 1) {
|
||||||
const uploadRes = await uni.uploadFile({
|
const uploadRes = await uni.uploadFile({
|
||||||
url: getApiUrl('/api/audio/upload'),
|
url: getApiUrl('/api/audio/upload'),
|
||||||
filePath: selectedFile.path,
|
filePath: selectedFile.path,
|
||||||
name: 'file',
|
name: 'file',
|
||||||
formData: {
|
formData: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
audioId: item.id,
|
audioId: item.id,
|
||||||
customerId: item.customerId || '',
|
customerId: item.customerId || '',
|
||||||
customerName: item.customerName || '',
|
customerName: item.customerName || '',
|
||||||
fileName: selectedFile.name || `service_file_${Date.now()}`
|
fileName: selectedFile.name || `service_file_${Date.now()}`,
|
||||||
},
|
compress: true
|
||||||
header: this.getAuthHeaders(),
|
},
|
||||||
timeout: 60000
|
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'
|
|
||||||
});
|
});
|
||||||
this.serviceStatusPage.current = 1;
|
let result = {};
|
||||||
this.fetchServiceStatusList({ force: true });
|
try {
|
||||||
return;
|
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({
|
this.updateUploadResult(recordId, resultId, {
|
||||||
title: result?.message || '上传失败',
|
status: 'failed',
|
||||||
icon: 'none'
|
statusText: '上传失败',
|
||||||
|
message: `${lastErrorMessage || '未知错误'}(已重试1次)`,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传文件失败:', error);
|
console.error('上传文件失败:', error);
|
||||||
uni.showToast({
|
this.updateUploadResult(recordId, resultId, {
|
||||||
title: error?.errMsg || error?.message || '上传失败',
|
status: 'failed',
|
||||||
icon: 'none'
|
statusText: '上传失败',
|
||||||
|
message: `${error?.errMsg || error?.message || '未知错误'}(已重试1次)`,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading();
|
this.decreaseUploadingCount(recordId);
|
||||||
this.uploadingIds = this.uploadingIds.filter(id => id !== recordId);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async finishService(item) {
|
async finishService(item) {
|
||||||
@@ -878,8 +1027,6 @@ export default {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.closeActionMenu();
|
|
||||||
|
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认结束',
|
title: '确认结束',
|
||||||
content: `确定要结束这条服务记录吗?`,
|
content: `确定要结束这条服务记录吗?`,
|
||||||
@@ -1285,92 +1432,65 @@ export default {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-trigger {
|
.service-status-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6rpx;
|
gap: 6rpx;
|
||||||
padding: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 10rpx;
|
|
||||||
background-color: #EEF4FF;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-trigger__primary {
|
.service-status-indicator__text {
|
||||||
padding: 6rpx 12rpx;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-trigger__primary-text {
|
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #007AFF;
|
color: #007AFF;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-actions-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.action-trigger__more {
|
.service-action-btn {
|
||||||
padding: 6rpx 10rpx 6rpx 6rpx;
|
padding: 8rpx 18rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.2;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-left: 1px solid rgba(0, 122, 255, 0.15);
|
border: 1px solid transparent;
|
||||||
background-color: transparent;
|
background-color: #F8FAFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-menu {
|
.service-action-btn--record {
|
||||||
position: absolute;
|
|
||||||
right: 24rpx;
|
|
||||||
top: 96rpx;
|
|
||||||
min-width: 220rpx;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
|
|
||||||
border: 1px solid #EEF0F3;
|
|
||||||
z-index: 50;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-menu-item {
|
|
||||||
padding: 22rpx 24rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-menu-item text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-menu-item:active {
|
|
||||||
background-color: #F5F7FA;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-menu-item--disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-menu-mask {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
z-index: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-status text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #007AFF;
|
color: #007AFF;
|
||||||
|
border-color: rgba(0, 122, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-action-btn--recording {
|
||||||
|
color: #FF3B30;
|
||||||
|
border-color: rgba(255, 59, 48, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-action-btn--supplement {
|
||||||
|
color: #10B981;
|
||||||
|
border-color: rgba(16, 185, 129, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-action-btn--upload {
|
||||||
|
color: #7C3AED;
|
||||||
|
border-color: rgba(124, 58, 237, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-action-btn--finish {
|
||||||
|
color: #EF4444;
|
||||||
|
border-color: rgba(239, 68, 68, 0.28);
|
||||||
|
background-color: #FFF7F7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-tags {
|
.customer-tags {
|
||||||
@@ -1543,6 +1663,45 @@ export default {
|
|||||||
color: #999;
|
color: #999;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recording-realtime {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-realtime text {
|
||||||
|
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 {
|
.supplement-dialog-mask {
|
||||||
|
|||||||
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