陪练和客户页面的产品化

This commit is contained in:
zhonghua1
2025-12-27 16:36:26 +08:00
parent f93affb4a4
commit 5c34bb86ac
2 changed files with 162 additions and 154 deletions

View File

@@ -17,51 +17,26 @@
<view class="content">
<!-- 标签页 -->
<scroll-view class="tabs-scroll" scroll-x="true" show-scrollbar="false">
<view class="tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'survey' }"
@click="switchTab('survey')">
<text>摸底</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'practice' }"
@click="switchTab('practice')">
<text>陪练</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'battle' }"
@click="switchTab('battle')">
<text>实战</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'quality' }"
@click="switchTab('quality')">
<text>质检</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'material' }"
@click="switchTab('material')">
<text>素材</text>
</view>
<view class="tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'practice' }"
@click="switchTab('practice')">
<text>陪练</text>
</view>
</scroll-view>
<!-- 摸底内容 -->
<scroll-view
class="tab-content"
scroll-y
v-if="activeTab === 'survey'">
<view class="champion-card">
<text class="champion-title">摸底</text>
<text class="champion-desc">这里是摸底相关的内容展示区域</text>
<view
class="tab-item"
:class="{ active: activeTab === 'quality' }"
@click="switchTab('quality')">
<text>质检</text>
</view>
</scroll-view>
<view
class="tab-item"
:class="{ active: activeTab === 'material' }"
@click="switchTab('material')">
<text>素材</text>
</view>
</view>
<!-- 陪练内容 -->
<scroll-view
@@ -108,7 +83,7 @@
</view>
<text class="staff-name">{{ item.title || '未命名知识' }}</text>
</view>
<view class="card-menu-trigger" @tap.stop="showPracticeContextMenu(item, index, $event)">
<view class="card-menu-trigger" @tap.stop="onPracticeActionBtnClick(item)">
<uni-icons type="more-filled" size="20" color="#999"></uni-icons>
</view>
</view>
@@ -126,16 +101,16 @@
</view>
</view>
<!-- 上下文菜单 -->
<!-- 操作菜单 -->
<view
class="context-menu"
v-if="practiceContextMenuVisible && practiceContextMenuIndex === index"
:style="{ top: practiceContextMenuTop + 'px', left: practiceContextMenuLeft + 'px' }"
class="practice-action-menu"
v-if="selectedPracticeItem && selectedPracticeItem.id === item.id && showPracticeActionMenu"
@tap.stop>
<view class="context-menu-item" @tap.stop="handleEditPractice(item)">
<view class="practice-action-menu-item" @tap.stop="handleEditPractice(item)">
<text>编辑</text>
</view>
<view class="context-menu-item context-menu-item--danger" @tap.stop="handleDeletePractice(item)">
<view class="practice-action-menu-divider"></view>
<view class="practice-action-menu-item practice-action-menu-item--danger" @tap.stop="handleDeletePractice(item)">
<text>删除</text>
</view>
</view>
@@ -304,17 +279,6 @@
</view>
</view>
<!-- 实战内容 -->
<scroll-view
class="tab-content"
scroll-y
v-if="activeTab === 'battle'">
<view class="champion-card">
<text class="champion-title">实战</text>
<text class="champion-desc">这里是实战相关的内容展示区域</text>
</view>
</scroll-view>
<!-- 质检内容 -->
<scroll-view
class="tab-content"
@@ -562,6 +526,13 @@
<!-- 上下文菜单遮罩层 -->
<view class="context-menu-mask" v-if="contextMenuVisible" @tap="closeContextMenu"></view>
<!-- 陪练操作菜单遮罩层 -->
<view
class="practice-action-menu-mask"
v-if="showPracticeActionMenu"
@tap="closePracticeActionMenu">
</view>
<!-- 内容详情弹出框 -->
<view class="content-popup-mask" v-if="showContentPopup" @click="closeContentPopup">
</view>
@@ -604,7 +575,7 @@
// #endif
data() {
return {
activeTab: 'survey',
activeTab: 'practice',
// 素材相关数据
materialList: [],
materialLoading: false,
@@ -676,11 +647,9 @@
content: '',
remark: ''
},
// 陪练上下文菜单
practiceContextMenuVisible: false,
practiceContextMenuIndex: -1,
practiceContextMenuTop: 0,
practiceContextMenuLeft: 0,
// 陪练操作菜单
selectedPracticeItem: null,
showPracticeActionMenu: false,
// 内容详情弹出框
showContentPopup: false,
contentPopupTitle: '',
@@ -1301,53 +1270,23 @@
this.practiceLoading = false;
}
},
showPracticeContextMenu(item, index, event) {
// 阻止事件冒泡
if (event && event.stopPropagation) {
event.stopPropagation();
}
// 获取系统信息以计算位置
const systemInfo = uni.getSystemInfoSync();
// 获取点击位置 - 优先使用触摸事件坐标
let top = 100;
let left = systemInfo.windowWidth - 180;
if (event) {
if (event.touches && event.touches.length > 0) {
// 触摸事件
top = event.touches[0].clientY;
left = event.touches[0].clientX - 80;
} else if (event.detail && typeof event.detail.x === 'number') {
// tap事件坐标单位px需要转换
top = event.detail.y;
left = event.detail.x - 80;
} else if (event.clientY !== undefined) {
// 鼠标事件
top = event.clientY;
left = event.clientX - 80;
}
onPracticeActionBtnClick(item) {
// 如果已经选中当前项,则关闭菜单
if (this.selectedPracticeItem && this.selectedPracticeItem.id === item.id && this.showPracticeActionMenu) {
this.closePracticeActionMenu();
return;
}
// 确保菜单不超出屏幕
if (left + 160 > systemInfo.windowWidth) {
left = systemInfo.windowWidth - 180;
}
if (left < 0) {
left = 10;
}
this.practiceContextMenuTop = top;
this.practiceContextMenuLeft = left;
// 显示菜单
this.practiceContextMenuIndex = index;
this.practiceContextMenuVisible = true;
this.selectedPracticeItem = item;
this.showPracticeActionMenu = true;
},
closePracticeContextMenu() {
this.practiceContextMenuVisible = false;
this.practiceContextMenuIndex = -1;
closePracticeActionMenu() {
this.showPracticeActionMenu = false;
this.selectedPracticeItem = null;
},
handleEditPractice(item) {
this.closePracticeContextMenu();
this.closePracticeActionMenu();
// 填充编辑表单
this.editPracticeForm = {
id: item.id,
@@ -1433,7 +1372,7 @@
}
},
handleDeletePractice(item) {
this.closePracticeContextMenu();
this.closePracticeActionMenu();
uni.showModal({
title: '提示',
content: '确认删除该陪练吗?',
@@ -1658,26 +1597,17 @@
}
/* 标签页 */
.tabs-scroll {
position: fixed;
.tabs {
display: flex;
background-color: #FFFFFF;
border-bottom: 1px solid #E0E0E0;
padding: 0 16rpx;
margin-top: 0;
position: absolute;
top: 88rpx; /* 导航栏高度 */
left: 0;
right: 0;
width: 100%;
background-color: #FFFFFF;
border-bottom: 1px solid #E0E0E0;
white-space: nowrap;
margin-top: 0;
z-index: 999;
height: 72rpx; /* 设置固定高度确保tabs可见 */
box-sizing: border-box;
}
.tabs {
display: inline-flex;
padding: 0 16rpx;
height: 100%; /* 确保tabs占满scroll-view的高度 */
align-items: center; /* 垂直居中 */
z-index: 1;
}
.tab-item {
@@ -1728,6 +1658,10 @@
left: 0;
right: 0;
bottom: 96rpx; /* 底部导航栏高度 */
background-color: #F5F5F5;
padding: 24rpx 16rpx 0 16rpx; /* 顶部24rpx间距与列表项间距一致底部0 */
box-sizing: border-box;
overflow-y: auto;
}
.champion-card {
@@ -1761,13 +1695,15 @@
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 2rpx;
padding: 4rpx 4rpx;
margin-bottom: 4rpx;
gap: 8rpx; /* 增加间距,避免元素被挤压 */
padding: 12rpx 8rpx; /* 增加内边距,给搜索框更多空间 */
margin-bottom: 24rpx; /* 与列表项间距保持一致24rpx */
background-color: #F8F8FA;
border-radius: 8rpx;
border: 1px solid #EFEFF2;
overflow-x: auto;
min-height: 64rpx; /* 设置最小高度,确保搜索区域不被挤压 */
box-sizing: border-box;
}
.toolbar-total {
@@ -1775,21 +1711,24 @@
color: #666;
white-space: nowrap;
flex-shrink: 0;
margin-right: 2rpx;
margin-right: 8rpx; /* 增加右边距,与搜索框保持距离 */
line-height: 1.2;
padding: 0 4rpx; /* 增加左右内边距 */
}
.toolbar-input {
flex: 1;
min-width: 100rpx;
height: 48rpx;
line-height: 48rpx;
min-width: 120rpx; /* 增加最小宽度,避免被挤压变形 */
max-width: 200rpx; /* 设置最大宽度,保持合理比例 */
height: 56rpx; /* 稍微增加高度,提升可用性 */
line-height: 56rpx;
background-color: #F5F6FA;
border-radius: 8rpx;
padding: 0 8rpx;
padding: 0 12rpx; /* 增加左右内边距 */
font-size: 24rpx;
border: 1px solid transparent;
box-sizing: border-box;
flex-shrink: 1; /* 允许适当收缩,但保持最小宽度 */
}
.toolbar-actions {
@@ -1797,21 +1736,26 @@
align-items: center;
margin-left: auto;
flex-shrink: 0;
gap: 2rpx;
gap: 8rpx; /* 增加按钮之间的间距 */
}
.toolbar-btn {
padding: 0 6rpx;
height: 48rpx;
min-width: 48rpx;
padding: 0 12rpx; /* 增加左右内边距 */
height: 56rpx; /* 与输入框高度保持一致 */
min-width: 56rpx;
color: #2A68FF;
display: flex;
align-items: center;
justify-content: center;
gap: 2rpx;
gap: 4rpx; /* 增加图标和文字之间的间距 */
font-size: 24rpx;
font-weight: 400;
line-height: 1;
box-sizing: border-box;
}
.toolbar-btn text {
color: #2A68FF;
}
.service-card {
@@ -1845,13 +1789,24 @@
}
.card-menu-trigger {
width: 48rpx;
height: 48rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
margin-left: auto;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
flex-grow: 0;
position: relative;
padding: 0;
}
.card-menu-trigger:active {
opacity: 0.7;
}
.context-menu {
@@ -1865,6 +1820,56 @@
max-width: 200rpx;
}
/* 陪练操作菜单样式 */
.practice-card-item {
position: relative;
}
.practice-action-menu {
position: absolute;
top: 60rpx;
right: 32rpx;
width: 160rpx;
background-color: #FFFFFF;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
z-index: 100;
overflow: hidden;
margin-right: 0;
}
.practice-action-menu-item {
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #333;
text-align: center;
background-color: #FFFFFF;
}
.practice-action-menu-item:active {
background-color: #F5F5F5;
}
.practice-action-menu-item--danger {
color: #FF5722;
}
.practice-action-menu-divider {
height: 1rpx;
background-color: #E0E0E0;
margin: 0 16rpx;
}
.practice-action-menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 99;
}
.context-menu-item {
padding: 24rpx 32rpx;
font-size: 28rpx;

View File

@@ -1632,33 +1632,36 @@
// 加载服务记录列表
async loadServiceList(params = {}) {
try {
// 构建查询参数对象(后端使用 @RequestParam参数需要作为 URL 查询参数传递)
const queryParams = {
current: this.serviceListPage.current,
size: this.serviceListPage.size
};
// 添加查询参数:所属销售姓名和客户姓名
if (params.salesName) {
queryParams.salesName = params.salesName;
const trimmedSalesName = params.salesName?.trim();
const trimmedCustomerName = params.customerName?.trim();
if (trimmedSalesName) {
queryParams.salesName = trimmedSalesName;
}
if (params.customerName) {
queryParams.customerName = params.customerName;
if (trimmedCustomerName) {
queryParams.customerName = trimmedCustomerName;
}
// 构建请求体(过滤空值),后端改为 POST
const requestData = Object.keys(queryParams)
// 将参数转换为 URL 查询字符串(因为后端使用 @RequestParam
const queryString = Object.keys(queryParams)
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
.reduce((acc, key) => {
acc[key] = queryParams[key];
return acc;
}, {});
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const url = getApiUrl('/api/audioManagement/list');
// 后端使用 @RequestParam参数需要作为 URL 查询参数传递
const baseUrl = getApiUrl('/api/audioManagement/list');
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
const res = await uni.request({
url: url,
url,
method: 'POST',
data: requestData,
data: {}, // POST 请求体为空,参数都在 URL 查询字符串中
header: {
'Content-Type': 'application/json'
},