增加录音功能调整页面布局。

This commit is contained in:
zhonghua.li
2026-04-16 09:20:13 +08:00
parent 826d9082ad
commit 8f556e2168
11 changed files with 222 additions and 48 deletions

View File

@@ -7,7 +7,7 @@ import envConfig from '@/common/env.js'
const API_TARGETS = { const API_TARGETS = {
local: 'http://localhost:8091/', local: 'http://localhost:8091/',
prod: 'https://api.huayang-star.com/' prod: 'http://api01ai.huayang-star.com/'
} }
const ENV_ALIAS = { const ENV_ALIAS = {

View File

@@ -3,6 +3,6 @@
* apiEnv 可选:'local' | 'prod' * apiEnv 可选:'local' | 'prod'
*/ */
export default { export default {
apiEnv: 'local' apiEnv: 'prod'
} }

View File

@@ -125,7 +125,7 @@ export function request(options = {}) {
}) })
// 每次请求打印完整 URL格式示例 // 每次请求打印完整 URL格式示例
// H5环境(local)直接使用完整URL: http://localhost:8090/api/audioManagement/list // H5环境(local)直接使用完整URL: http://localhost:8091/api/audioManagement/list
console.log(`[Request] 完整请求URL: ${fullUrl}`) console.log(`[Request] 完整请求URL: ${fullUrl}`)
// 构建请求头 // 构建请求头

View File

@@ -380,7 +380,7 @@ export default {
} }
// 上传到服务器需要根据实际API调整 // 上传到服务器需要根据实际API调整
const url = getApiUrl('/api/audioManagement/uploadBluetoothAudio') const url = getApiUrl('/api/audioManagement/upload')
const res = await post(url, formData, { const res = await post(url, formData, {
header: { header: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'

View File

@@ -21,15 +21,15 @@
<view class="tabs" :style="{ top: computedNavbarTop || navbarTop }"> <view class="tabs" :style="{ top: computedNavbarTop || navbarTop }">
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'service' }" :class="{ active: activeTab === 'customer' }"
@click="switchTab('service')"> @click="switchTab('customer')">
<text>服务记录</text> <text>客户列表</text>
</view> </view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'customer' }" :class="{ active: activeTab === 'service' }"
@click="switchTab('customer')"> @click="switchTab('service')">
<text>客户</text> <text>服务记录</text>
</view> </view>
</view> </view>
@@ -518,8 +518,9 @@
return (totalNavbarHeight + 72) + 'rpx'; // tab栏高度72rpx return (totalNavbarHeight + 72) + 'rpx'; // tab栏高度72rpx
} }
}, },
onLoad() { onLoad(options = {}) {
console.log('[FurnitureCustomer][onLoad] 页面加载'); console.log('[FurnitureCustomer][onLoad] 页面加载');
this.applyIncomingTab(options?.tab);
// 更新导航栏位置 // 更新导航栏位置
this.updateNavbarPosition(); this.updateNavbarPosition();
// 加载服务记录列表 // 加载服务记录列表
@@ -618,6 +619,13 @@
this.playingAudioId = null; this.playingAudioId = null;
}, },
methods: { methods: {
applyIncomingTab(tab) {
const allowedTabs = ['service', 'customer'];
if (!allowedTabs.includes(tab)) {
return;
}
this.activeTab = tab;
},
// 计算并更新导航栏和内容区域的位置 // 计算并更新导航栏和内容区域的位置
updateNavbarPosition() { updateNavbarPosition() {
console.log('[FurnitureCustomer][updateNavbarPosition] 开始更新导航栏位置'); console.log('[FurnitureCustomer][updateNavbarPosition] 开始更新导航栏位置');

View File

@@ -443,8 +443,7 @@ export default {
detailedAddress: this.formData.detailedAddress?.trim() || "", detailedAddress: this.formData.detailedAddress?.trim() || "",
salesPhone: trimmedSalesPhone || "", salesPhone: trimmedSalesPhone || "",
contactCount: Number(this.formData.contactCount) || 0, contactCount: Number(this.formData.contactCount) || 0,
operationType: action, operationType: action
scenario: "furniture"
}; };
try { try {
@@ -455,9 +454,13 @@ export default {
// 获取认证信息 // 获取认证信息
let tenantId = ''; let tenantId = '';
let token = ''; let token = '';
let scenario = '';
try { try {
const loginResponse = uni.getStorageSync('backend-login-response') || {};
tenantId = uni.getStorageSync('backend-tenant-id') || ''; tenantId = uni.getStorageSync('backend-tenant-id') || '';
token = uni.getStorageSync('backend-token') || ''; token = uni.getStorageSync('backend-token') || '';
// 场景值来自登录返回,兜底读取历史缓存字段
scenario = (loginResponse.scenario || uni.getStorageSync('backend-scenario') || '').toString().trim();
} catch (e) { } catch (e) {
console.error('获取认证信息失败:', e); console.error('获取认证信息失败:', e);
} }
@@ -472,6 +475,10 @@ export default {
if (tenantId) { if (tenantId) {
headers['X-Tenant-Id'] = tenantId; headers['X-Tenant-Id'] = tenantId;
} }
if (scenario) {
headers['X-Scenario'] = scenario;
params.scenario = scenario;
}
const res = await uni.request({ const res = await uni.request({
url: getApiUrl('/api/customerManagement/add'), url: getApiUrl('/api/customerManagement/add'),

View File

@@ -19,18 +19,18 @@
<view class="content"> <view class="content">
<!-- 标签页 --> <!-- 标签页 -->
<view class="tabs" :style="{ top: computedNavbarTop || navbarTop }"> <view class="tabs" :style="{ top: computedNavbarTop || navbarTop }">
<view
class="tab-item"
:class="{ active: activeTab === 'status' }"
@click="switchTab('status')">
<text>服务中</text>
</view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'reception' }" :class="{ active: activeTab === 'reception' }"
@click="switchTab('reception')"> @click="switchTab('reception')">
<text>开始接待</text> <text>开始接待</text>
</view> </view>
<view
class="tab-item"
:class="{ active: activeTab === 'status' }"
@click="switchTab('status')">
<text>服务中</text>
</view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'tag' }" :class="{ active: activeTab === 'tag' }"

View File

@@ -52,6 +52,20 @@
<text class="staff-name">{{ item.staffName || '未分配销售' }}</text> <text class="staff-name">{{ item.staffName || '未分配销售' }}</text>
</view> </view>
<view class="service-status"> <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)"> <view class="service-action-btn service-action-btn--supplement" @click.stop="showSupplementDialog(item)">
<text>补录</text> <text>补录</text>
</view> </view>
@@ -212,7 +226,11 @@ export default {
contact: "", contact: "",
recordingName: "", recordingName: "",
remarks: "" remarks: ""
} },
recorderSupported: false,
recorderManager: null,
recordingServiceId: null,
recordingContext: null
} }
}, },
mounted() { mounted() {
@@ -220,9 +238,102 @@ export default {
this.checkUserRole(); this.checkUserRole();
// 获取当前用户手机号 // 获取当前用户手机号
this.loadCurrentUserPhone(); this.loadCurrentUserPhone();
this.initServiceRecorder();
this.fetchServiceStatusList(); this.fetchServiceStatusList();
}, },
beforeUnmount() {
if (this.recorderManager && this.recordingServiceId) {
try {
this.recorderManager.stop();
} catch (e) {
/* ignore */
}
}
},
methods: { 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字符串如admin_furniture就视为admin角色 * 只要角色中包含admin字符串如admin_furniture就视为admin角色
@@ -467,6 +578,22 @@ export default {
if (!recordId) return false; if (!recordId) return false;
return this.uploadingIds.includes(String(recordId)); 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) { async chooseAndUploadFile(item) {
if (!item || !item.id) { if (!item || !item.id) {
uni.showToast({ uni.showToast({
@@ -508,6 +635,14 @@ export default {
resolve(null); resolve(null);
return; return;
} }
if (!this.isMp3UploadFile(selected)) {
uni.showToast({
title: '仅支持上传 MP3 文件',
icon: 'none'
});
resolve(null);
return;
}
resolve(selected); resolve(selected);
}; };
const onFail = (err) => { const onFail = (err) => {
@@ -535,6 +670,7 @@ export default {
if (typeof uni.chooseFile === 'function') { if (typeof uni.chooseFile === 'function') {
uni.chooseFile({ uni.chooseFile({
count: 1, count: 1,
extension: ['.mp3'],
success: onSuccess, success: onSuccess,
fail: onFail fail: onFail
}); });
@@ -573,11 +709,12 @@ export default {
title: '上传中...' title: '上传中...'
}); });
const uploadRes = await uni.uploadFile({ const uploadRes = await uni.uploadFile({
url: getApiUrl('/api/audioManagement/uploadBluetoothAudio'), 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,
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()}`
@@ -1210,6 +1347,19 @@ export default {
opacity: 0.7; 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 { .service-status-empty {
padding: 48rpx 0; padding: 48rpx 0;
text-align: center; text-align: center;

View File

@@ -18,47 +18,47 @@
<view class="content"> <view class="content">
<!-- 功能分类展示 --> <!-- 功能分类展示 -->
<scroll-view class="content-scroll" scroll-y="true" :style="{ top: computedContentTop || contentTop, bottom: contentBottom || '96rpx' }"> <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-section quick-links-section">
<view class="category-header"> <view class="category-header">
<text class="category-icon">🚀</text> <text class="category-icon">🚀</text>
<text class="category-title">接待页面快捷入口</text> <text class="category-title">接待</text>
</view> </view>
<view class="function-grid"> <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-item" @click="quickJumpReceptionTab('reception')">
<view class="function-icon">🤝</view> <view class="function-icon">🤝</view>
<text class="function-name">开始接待</text> <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>
<view class="function-item" @click="quickJumpReceptionTab('tag')"> <view class="function-item" @click="quickJumpReceptionTab('tag')">
<view class="function-icon">🏷</view> <view class="function-icon">🏷</view>
<text class="function-name">标签管理</text> <text class="function-name">标签管理</text>
<text class="function-desc">跳转到接待页标签管理tab</text> <text class="function-desc"></text>
</view> </view>
</view> </view>
</view> </view>
<!-- 客户页面快捷入口 --> <!-- AI分析 -->
<view class="category-section two-column"> <view class="category-section two-column">
<view class="category-header"> <view class="category-header">
<text class="category-icon">👤</text> <text class="category-icon">👤</text>
<text class="category-title">客户页面快捷入口</text> <text class="category-title">AI分析</text>
</view> </view>
<view class="function-grid"> <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-item" @click="quickJumpCustomerTab('customer')">
<view class="function-icon">👥</view> <view class="function-icon">👥</view>
<text class="function-name">客户列表</text> <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> </view>
</view> </view>
@@ -73,12 +73,12 @@
<view class="function-item" @click="navigateToPage('recording_duration')"> <view class="function-item" @click="navigateToPage('recording_duration')">
<view class="function-icon"></view> <view class="function-icon"></view>
<text class="function-name">录音时长</text> <text class="function-name">录音时长</text>
<text class="function-desc">统计录音总时长</text> <text class="function-desc"></text>
</view> </view>
<view class="function-item" @click="navigateToPage('customer_count')"> <view class="function-item" @click="navigateToPage('customer_count')">
<view class="function-icon">👥</view> <view class="function-icon">👥</view>
<text class="function-name">接待客户数</text> <text class="function-name">接待客户数</text>
<text class="function-desc">统计接待客户数量</text> <text class="function-desc"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -93,12 +93,12 @@
<view class="function-item" @click="showQualityCoveragePopup"> <view class="function-item" @click="showQualityCoveragePopup">
<view class="function-icon">📊</view> <view class="function-icon">📊</view>
<text class="function-name">质检覆盖率</text> <text class="function-name">质检覆盖率</text>
<text class="function-desc">质检录音覆盖统计</text> <text class="function-desc"></text>
</view> </view>
<view class="function-item" @click="navigateToPage('speech_rating')"> <view class="function-item" @click="navigateToPage('speech_rating')">
<view class="function-icon">🗣</view> <view class="function-icon">🗣</view>
<text class="function-name">员工话术评分</text> <text class="function-name">员工话术评分</text>
<text class="function-desc">员工沟通质量评分</text> <text class="function-desc"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -113,17 +113,17 @@
<view class="function-item full-row-item" @click="navigateToPage('intent_level')"> <view class="function-item full-row-item" @click="navigateToPage('intent_level')">
<view class="function-icon">🎯</view> <view class="function-icon">🎯</view>
<text class="function-name">客户意向分级</text> <text class="function-name">客户意向分级</text>
<text class="function-desc">客户购买意向分析</text> <text class="function-desc"></text>
</view> </view>
<view class="function-item full-row-item" @click="navigateToPage('deal_attribution')"> <view class="function-item full-row-item" @click="navigateToPage('deal_attribution')">
<view class="function-icon">💰</view> <view class="function-icon">💰</view>
<text class="function-name">成交归因</text> <text class="function-name">成交归因</text>
<text class="function-desc">成交成功因素分析</text> <text class="function-desc"></text>
</view> </view>
<view class="function-item full-row-item new-line-item" @click="navigateToPage('opening_rate')"> <view class="function-item full-row-item new-line-item" @click="navigateToPage('opening_rate')">
<view class="function-icon">🚪</view> <view class="function-icon">🚪</view>
<text class="function-name">开口率</text> <text class="function-name">开口率</text>
<text class="function-desc">客户主动开口统计</text> <text class="function-desc"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -138,12 +138,12 @@
<view class="function-item" @click="navigateToPage('over_commitment')"> <view class="function-item" @click="navigateToPage('over_commitment')">
<view class="function-icon">🚨</view> <view class="function-icon">🚨</view>
<text class="function-name">过度承诺统计</text> <text class="function-name">过度承诺统计</text>
<text class="function-desc">监控过度承诺情况</text> <text class="function-desc"></text>
</view> </view>
<view class="function-item" @click="navigateToPage('red_line_touch')"> <view class="function-item" @click="navigateToPage('red_line_touch')">
<view class="function-icon">🔴</view> <view class="function-icon">🔴</view>
<text class="function-name">触及红线统计</text> <text class="function-name">触及红线统计</text>
<text class="function-desc">违规行为监控统计</text> <text class="function-desc"></text>
</view> </view>
</view> </view>
</view> </view>

View File

@@ -26,7 +26,11 @@ export default {
const allowed = ['status', 'reception', 'tag']; const allowed = ['status', 'reception', 'tag'];
const tab = (uni.getStorageSync('workbench-reception-tab') || '').toString(); const tab = (uni.getStorageSync('workbench-reception-tab') || '').toString();
if (allowed.includes(tab)) { if (allowed.includes(tab)) {
this.incomingTab = tab; // 先清空再赋值,确保同一个 tabKey 也能触发子组件 watcher
this.incomingTab = '';
this.$nextTick(() => {
this.incomingTab = tab;
});
uni.removeStorageSync('workbench-reception-tab'); uni.removeStorageSync('workbench-reception-tab');
} }
} catch (e) { } catch (e) {

View File

@@ -27,6 +27,10 @@
<text class="info-label">当前角色</text> <text class="info-label">当前角色</text>
<text class="info-value">{{loginUserInfo.roleName || '未设置'}}</text> <text class="info-value">{{loginUserInfo.roleName || '未设置'}}</text>
</view> </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"> <view class="info-row" v-if="loginUserInfo.phone">
<text class="info-label">手机号</text> <text class="info-label">手机号</text>
<text class="info-value">{{loginUserInfo.phone}}</text> <text class="info-value">{{loginUserInfo.phone}}</text>
@@ -299,6 +303,7 @@
this.loginUserInfo = { this.loginUserInfo = {
userName: loginResponse.userName || '', userName: loginResponse.userName || '',
roleName: loginResponse.roleName || '', roleName: loginResponse.roleName || '',
scenario: loginResponse.scenario || uni.getStorageSync('backend-scenario') || '',
phone: loginResponse.phone || '', phone: loginResponse.phone || '',
email: loginResponse.email || '', email: loginResponse.email || '',
userId: loginResponse.userId || '' userId: loginResponse.userId || ''