增加录音功能调整页面布局。
This commit is contained in:
@@ -7,7 +7,7 @@ import envConfig from '@/common/env.js'
|
||||
|
||||
const API_TARGETS = {
|
||||
local: 'http://localhost:8091/',
|
||||
prod: 'https://api.huayang-star.com/'
|
||||
prod: 'http://api01ai.huayang-star.com/'
|
||||
}
|
||||
|
||||
const ENV_ALIAS = {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
* apiEnv 可选:'local' | 'prod'
|
||||
*/
|
||||
export default {
|
||||
apiEnv: 'local'
|
||||
apiEnv: 'prod'
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export function request(options = {}) {
|
||||
})
|
||||
|
||||
// 每次请求打印完整 URL,格式示例:
|
||||
// H5环境(local)直接使用完整URL: http://localhost:8090/api/audioManagement/list
|
||||
// H5环境(local)直接使用完整URL: http://localhost:8091/api/audioManagement/list
|
||||
console.log(`[Request] 完整请求URL: ${fullUrl}`)
|
||||
|
||||
// 构建请求头
|
||||
|
||||
@@ -380,7 +380,7 @@ export default {
|
||||
}
|
||||
|
||||
// 上传到服务器(需要根据实际API调整)
|
||||
const url = getApiUrl('/api/audioManagement/uploadBluetoothAudio')
|
||||
const url = getApiUrl('/api/audioManagement/upload')
|
||||
const res = await post(url, formData, {
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
<view class="tabs" :style="{ top: computedNavbarTop || navbarTop }">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'service' }"
|
||||
@click="switchTab('service')">
|
||||
<text>服务记录</text>
|
||||
:class="{ active: activeTab === 'customer' }"
|
||||
@click="switchTab('customer')">
|
||||
<text>客户列表</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'customer' }"
|
||||
@click="switchTab('customer')">
|
||||
<text>客户</text>
|
||||
:class="{ active: activeTab === 'service' }"
|
||||
@click="switchTab('service')">
|
||||
<text>服务记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -518,8 +518,9 @@
|
||||
return (totalNavbarHeight + 72) + 'rpx'; // tab栏高度72rpx
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
onLoad(options = {}) {
|
||||
console.log('[FurnitureCustomer][onLoad] 页面加载');
|
||||
this.applyIncomingTab(options?.tab);
|
||||
// 更新导航栏位置
|
||||
this.updateNavbarPosition();
|
||||
// 加载服务记录列表
|
||||
@@ -618,6 +619,13 @@
|
||||
this.playingAudioId = null;
|
||||
},
|
||||
methods: {
|
||||
applyIncomingTab(tab) {
|
||||
const allowedTabs = ['service', 'customer'];
|
||||
if (!allowedTabs.includes(tab)) {
|
||||
return;
|
||||
}
|
||||
this.activeTab = tab;
|
||||
},
|
||||
// 计算并更新导航栏和内容区域的位置
|
||||
updateNavbarPosition() {
|
||||
console.log('[FurnitureCustomer][updateNavbarPosition] 开始更新导航栏位置');
|
||||
|
||||
@@ -443,8 +443,7 @@ export default {
|
||||
detailedAddress: this.formData.detailedAddress?.trim() || "",
|
||||
salesPhone: trimmedSalesPhone || "",
|
||||
contactCount: Number(this.formData.contactCount) || 0,
|
||||
operationType: action,
|
||||
scenario: "furniture"
|
||||
operationType: action
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -455,9 +454,13 @@ export default {
|
||||
// 获取认证信息
|
||||
let tenantId = '';
|
||||
let token = '';
|
||||
let scenario = '';
|
||||
try {
|
||||
const loginResponse = uni.getStorageSync('backend-login-response') || {};
|
||||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||||
token = uni.getStorageSync('backend-token') || '';
|
||||
// 场景值来自登录返回,兜底读取历史缓存字段
|
||||
scenario = (loginResponse.scenario || uni.getStorageSync('backend-scenario') || '').toString().trim();
|
||||
} catch (e) {
|
||||
console.error('获取认证信息失败:', e);
|
||||
}
|
||||
@@ -472,6 +475,10 @@ export default {
|
||||
if (tenantId) {
|
||||
headers['X-Tenant-Id'] = tenantId;
|
||||
}
|
||||
if (scenario) {
|
||||
headers['X-Scenario'] = scenario;
|
||||
params.scenario = scenario;
|
||||
}
|
||||
|
||||
const res = await uni.request({
|
||||
url: getApiUrl('/api/customerManagement/add'),
|
||||
|
||||
@@ -19,18 +19,18 @@
|
||||
<view class="content">
|
||||
<!-- 标签页 -->
|
||||
<view class="tabs" :style="{ top: computedNavbarTop || navbarTop }">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'status' }"
|
||||
@click="switchTab('status')">
|
||||
<text>服务中</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'reception' }"
|
||||
@click="switchTab('reception')">
|
||||
<text>开始接待</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'status' }"
|
||||
@click="switchTab('status')">
|
||||
<text>服务中</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'tag' }"
|
||||
|
||||
@@ -52,6 +52,20 @@
|
||||
<text class="staff-name">{{ item.staffName || '未分配销售' }}</text>
|
||||
</view>
|
||||
<view class="service-status">
|
||||
<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--record 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>
|
||||
@@ -212,7 +226,11 @@ export default {
|
||||
contact: "",
|
||||
recordingName: "",
|
||||
remarks: ""
|
||||
}
|
||||
},
|
||||
recorderSupported: false,
|
||||
recorderManager: null,
|
||||
recordingServiceId: null,
|
||||
recordingContext: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -220,9 +238,102 @@ export default {
|
||||
this.checkUserRole();
|
||||
// 获取当前用户手机号
|
||||
this.loadCurrentUserPhone();
|
||||
this.initServiceRecorder();
|
||||
this.fetchServiceStatusList();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.recorderManager && this.recordingServiceId) {
|
||||
try {
|
||||
this.recorderManager.stop();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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;
|
||||
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.recorderManager.onError(() => {
|
||||
this.recordingServiceId = null;
|
||||
this.recordingContext = null;
|
||||
uni.showToast({ title: '录音出错', icon: 'none' });
|
||||
});
|
||||
},
|
||||
isRecordingItem(recordId) {
|
||||
if (!recordId || !this.recordingServiceId) {
|
||||
return false;
|
||||
}
|
||||
return String(recordId) === this.recordingServiceId;
|
||||
},
|
||||
startPhoneRecord(item) {
|
||||
if (!this.recorderSupported || !this.recorderManager) {
|
||||
uni.showToast({
|
||||
title: '当前环境不支持录音,请使用微信小程序或 App',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!item || !item.id) {
|
||||
uni.showToast({
|
||||
title: '无法获取服务记录ID',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.recordingServiceId) {
|
||||
uni.showToast({ title: '请先停止当前录音', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (this.isUploading(item.id)) {
|
||||
uni.showToast({ title: '正在上传,请稍候', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
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' });
|
||||
}
|
||||
},
|
||||
/** 结束录音;onStop 回调中会调用 uploadFileForRecord 上传至后端 */
|
||||
stopPhoneRecordAndUpload(item) {
|
||||
if (!this.recorderManager || !item?.id) {
|
||||
return;
|
||||
}
|
||||
if (String(item.id) !== this.recordingServiceId) {
|
||||
return;
|
||||
}
|
||||
this.recorderManager.stop();
|
||||
},
|
||||
/**
|
||||
* 检查用户角色,判断是否包含admin
|
||||
* 只要角色中包含admin字符串(如admin_furniture),就视为admin角色
|
||||
@@ -467,6 +578,22 @@ export default {
|
||||
if (!recordId) return false;
|
||||
return this.uploadingIds.includes(String(recordId));
|
||||
},
|
||||
/** 仅允许扩展名为 .mp3 的文件(不区分大小写) */
|
||||
isMp3UploadFile(selected) {
|
||||
if (!selected || !selected.path) return false;
|
||||
const pickBase = (s) => {
|
||||
if (!s || typeof s !== 'string') return '';
|
||||
const seg = s.split('/').pop() || s.split('\\').pop() || s;
|
||||
return seg.trim();
|
||||
};
|
||||
const bases = [pickBase(selected.name), pickBase(selected.path)].filter(Boolean);
|
||||
for (const base of bases) {
|
||||
if (base.toLowerCase().endsWith('.mp3')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async chooseAndUploadFile(item) {
|
||||
if (!item || !item.id) {
|
||||
uni.showToast({
|
||||
@@ -508,6 +635,14 @@ export default {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
if (!this.isMp3UploadFile(selected)) {
|
||||
uni.showToast({
|
||||
title: '仅支持上传 MP3 文件',
|
||||
icon: 'none'
|
||||
});
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(selected);
|
||||
};
|
||||
const onFail = (err) => {
|
||||
@@ -535,6 +670,7 @@ export default {
|
||||
if (typeof uni.chooseFile === 'function') {
|
||||
uni.chooseFile({
|
||||
count: 1,
|
||||
extension: ['.mp3'],
|
||||
success: onSuccess,
|
||||
fail: onFail
|
||||
});
|
||||
@@ -573,11 +709,12 @@ export default {
|
||||
title: '上传中...'
|
||||
});
|
||||
const uploadRes = await uni.uploadFile({
|
||||
url: getApiUrl('/api/audioManagement/uploadBluetoothAudio'),
|
||||
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()}`
|
||||
@@ -1210,6 +1347,19 @@ export default {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.service-action-btn--record {
|
||||
color: #007AFF;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.service-action-btn--record:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.service-action-btn--recording {
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.service-status-empty {
|
||||
padding: 48rpx 0;
|
||||
text-align: center;
|
||||
|
||||
@@ -18,47 +18,47 @@
|
||||
<view class="content">
|
||||
<!-- 功能分类展示 -->
|
||||
<scroll-view class="content-scroll" scroll-y="true" :style="{ top: computedContentTop || contentTop, bottom: contentBottom || '96rpx' }">
|
||||
<!-- 接待页面快捷入口 -->
|
||||
<!-- 接待 -->
|
||||
<view class="category-section quick-links-section">
|
||||
<view class="category-header">
|
||||
<text class="category-icon">🚀</text>
|
||||
<text class="category-title">接待页面快捷入口</text>
|
||||
<text class="category-title">接待</text>
|
||||
</view>
|
||||
<view class="function-grid">
|
||||
<view class="function-item" @click="quickJumpReceptionTab('status')">
|
||||
<view class="function-icon">📋</view>
|
||||
<text class="function-name">服务状态</text>
|
||||
<text class="function-desc">跳转到接待页服务状态tab</text>
|
||||
</view>
|
||||
<view class="function-item" @click="quickJumpReceptionTab('reception')">
|
||||
<view class="function-icon">🤝</view>
|
||||
<text class="function-name">开始接待</text>
|
||||
<text class="function-desc">跳转到接待页开始接待tab</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item" @click="quickJumpReceptionTab('status')">
|
||||
<view class="function-icon">📋</view>
|
||||
<text class="function-name">服务状态</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item" @click="quickJumpReceptionTab('tag')">
|
||||
<view class="function-icon">🏷️</view>
|
||||
<text class="function-name">标签管理</text>
|
||||
<text class="function-desc">跳转到接待页标签管理tab</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户页面快捷入口 -->
|
||||
<!-- AI分析 -->
|
||||
<view class="category-section two-column">
|
||||
<view class="category-header">
|
||||
<text class="category-icon">👤</text>
|
||||
<text class="category-title">客户页面快捷入口</text>
|
||||
<text class="category-title">AI分析</text>
|
||||
</view>
|
||||
<view class="function-grid">
|
||||
<view class="function-item" @click="quickJumpCustomerTab('service')">
|
||||
<view class="function-icon">📝</view>
|
||||
<text class="function-name">服务记录</text>
|
||||
<text class="function-desc">跳转到客户页服务记录tab</text>
|
||||
</view>
|
||||
<view class="function-item" @click="quickJumpCustomerTab('customer')">
|
||||
<view class="function-icon">👥</view>
|
||||
<text class="function-name">客户列表</text>
|
||||
<text class="function-desc">跳转到客户页客户列表tab</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item" @click="quickJumpCustomerTab('service')">
|
||||
<view class="function-icon">📝</view>
|
||||
<text class="function-name">服务记录</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -73,12 +73,12 @@
|
||||
<view class="function-item" @click="navigateToPage('recording_duration')">
|
||||
<view class="function-icon">⏱️</view>
|
||||
<text class="function-name">录音时长</text>
|
||||
<text class="function-desc">统计录音总时长</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item" @click="navigateToPage('customer_count')">
|
||||
<view class="function-icon">👥</view>
|
||||
<text class="function-name">接待客户数</text>
|
||||
<text class="function-desc">统计接待客户数量</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -93,12 +93,12 @@
|
||||
<view class="function-item" @click="showQualityCoveragePopup">
|
||||
<view class="function-icon">📊</view>
|
||||
<text class="function-name">质检覆盖率</text>
|
||||
<text class="function-desc">质检录音覆盖统计</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item" @click="navigateToPage('speech_rating')">
|
||||
<view class="function-icon">🗣️</view>
|
||||
<text class="function-name">员工话术评分</text>
|
||||
<text class="function-desc">员工沟通质量评分</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -113,17 +113,17 @@
|
||||
<view class="function-item full-row-item" @click="navigateToPage('intent_level')">
|
||||
<view class="function-icon">🎯</view>
|
||||
<text class="function-name">客户意向分级</text>
|
||||
<text class="function-desc">客户购买意向分析</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item full-row-item" @click="navigateToPage('deal_attribution')">
|
||||
<view class="function-icon">💰</view>
|
||||
<text class="function-name">成交归因</text>
|
||||
<text class="function-desc">成交成功因素分析</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item full-row-item new-line-item" @click="navigateToPage('opening_rate')">
|
||||
<view class="function-icon">🚪</view>
|
||||
<text class="function-name">开口率</text>
|
||||
<text class="function-desc">客户主动开口统计</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -138,12 +138,12 @@
|
||||
<view class="function-item" @click="navigateToPage('over_commitment')">
|
||||
<view class="function-icon">🚨</view>
|
||||
<text class="function-name">过度承诺统计</text>
|
||||
<text class="function-desc">监控过度承诺情况</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
<view class="function-item" @click="navigateToPage('red_line_touch')">
|
||||
<view class="function-icon">🔴</view>
|
||||
<text class="function-name">触及红线统计</text>
|
||||
<text class="function-desc">违规行为监控统计</text>
|
||||
<text class="function-desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -26,7 +26,11 @@ export default {
|
||||
const allowed = ['status', 'reception', 'tag'];
|
||||
const tab = (uni.getStorageSync('workbench-reception-tab') || '').toString();
|
||||
if (allowed.includes(tab)) {
|
||||
this.incomingTab = tab;
|
||||
// 先清空再赋值,确保同一个 tabKey 也能触发子组件 watcher
|
||||
this.incomingTab = '';
|
||||
this.$nextTick(() => {
|
||||
this.incomingTab = tab;
|
||||
});
|
||||
uni.removeStorageSync('workbench-reception-tab');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
<text class="info-label">当前角色:</text>
|
||||
<text class="info-value">{{loginUserInfo.roleName || '未设置'}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">场景:</text>
|
||||
<text class="info-value">{{loginUserInfo.scenario || '未设置'}}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="loginUserInfo.phone">
|
||||
<text class="info-label">手机号:</text>
|
||||
<text class="info-value">{{loginUserInfo.phone}}</text>
|
||||
@@ -299,6 +303,7 @@
|
||||
this.loginUserInfo = {
|
||||
userName: loginResponse.userName || '',
|
||||
roleName: loginResponse.roleName || '',
|
||||
scenario: loginResponse.scenario || uni.getStorageSync('backend-scenario') || '',
|
||||
phone: loginResponse.phone || '',
|
||||
email: loginResponse.email || '',
|
||||
userId: loginResponse.userId || ''
|
||||
|
||||
Reference in New Issue
Block a user