Compare commits
3 Commits
f373f591dd
...
87afece84f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87afece84f | ||
|
|
e7976c0993 | ||
|
|
811f5ad16b |
@@ -62,46 +62,9 @@
|
||||
</view>
|
||||
<text class="staff-name">{{ item.staffName || '未分配销售' }}</text>
|
||||
</view>
|
||||
<view class="service-status">
|
||||
<view class="action-trigger">
|
||||
<view class="action-trigger__primary" @click.stop="finishService(item)">
|
||||
<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 class="service-status-indicator">
|
||||
<uni-icons type="bars" size="16" color="#007AFF"></uni-icons>
|
||||
<text class="service-status-indicator__text">{{ recordStateLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -145,6 +108,47 @@
|
||||
<view class="service-duration">
|
||||
<text>{{ item.durationText }}</text>
|
||||
</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 class="service-status-empty" v-if="!serviceStatusLoading && !serviceStatusList.length">
|
||||
<text>{{ emptyListHint }}</text>
|
||||
@@ -154,12 +158,6 @@
|
||||
</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" @click.stop>
|
||||
@@ -287,7 +285,20 @@ export default {
|
||||
recorderManager: null,
|
||||
recordingServiceId: 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() {
|
||||
@@ -299,6 +310,8 @@ export default {
|
||||
this.fetchServiceStatusList();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopRecordingTicker();
|
||||
this.setKeepScreenOn(false);
|
||||
if (this.recorderManager && this.recordingServiceId) {
|
||||
try {
|
||||
this.recorderManager.stop();
|
||||
@@ -308,79 +321,160 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.recorderSupported = typeof uni.getRecorderManager === 'function';
|
||||
if (!this.recorderSupported) {
|
||||
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;
|
||||
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.recordingManualStopRequested = false;
|
||||
this.recordingCurrentSegmentNo = 0;
|
||||
this.recordingCurrentSegmentStartAt = 0;
|
||||
this.recordingCurrentSegmentElapsedSeconds = 0;
|
||||
this.setKeepScreenOn(false);
|
||||
});
|
||||
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' });
|
||||
});
|
||||
},
|
||||
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) {
|
||||
if (!recordId || !this.recordingServiceId) {
|
||||
return false;
|
||||
@@ -410,21 +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'
|
||||
});
|
||||
} catch (e) {
|
||||
this.recordingServiceId = null;
|
||||
this.recordingContext = null;
|
||||
uni.showToast({ title: '无法开始录音', icon: 'none' });
|
||||
}
|
||||
this.setKeepScreenOn(true);
|
||||
this.startNewSegmentRecording();
|
||||
},
|
||||
/** 结束录音;onStop 回调中会调用 uploadFileForRecord 上传至后端 */
|
||||
stopPhoneRecordAndUpload(item) {
|
||||
@@ -434,6 +522,7 @@ export default {
|
||||
if (String(item.id) !== this.recordingServiceId) {
|
||||
return;
|
||||
}
|
||||
this.recordingManualStopRequested = true;
|
||||
this.recorderManager.stop();
|
||||
},
|
||||
/**
|
||||
@@ -693,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) {
|
||||
@@ -818,56 +941,82 @@ export default {
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
async uploadFileForRecord(item, selectedFile) {
|
||||
async uploadFileForRecord(item, selectedFile, options = {}) {
|
||||
const recordId = String(item.id);
|
||||
this.closeActionMenu();
|
||||
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) {
|
||||
@@ -878,8 +1027,6 @@ export default {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.closeActionMenu();
|
||||
|
||||
uni.showModal({
|
||||
title: '确认结束',
|
||||
content: `确定要结束这条服务记录吗?`,
|
||||
@@ -1285,92 +1432,65 @@ export default {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.service-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.action-trigger {
|
||||
.service-status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #EEF4FF;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-trigger__primary {
|
||||
padding: 6rpx 12rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.action-trigger__primary-text {
|
||||
.service-status-indicator__text {
|
||||
font-size: 26rpx;
|
||||
color: #007AFF;
|
||||
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 {
|
||||
padding: 6rpx 10rpx 6rpx 6rpx;
|
||||
.service-action-btn {
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid rgba(0, 122, 255, 0.15);
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
background-color: #F8FAFF;
|
||||
}
|
||||
|
||||
.action-menu {
|
||||
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;
|
||||
.service-action-btn--record {
|
||||
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 {
|
||||
@@ -1543,6 +1663,45 @@ export default {
|
||||
color: #999;
|
||||
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 {
|
||||
|
||||
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