陪练答题

This commit is contained in:
zhonghua1
2025-12-27 23:32:50 +08:00
parent 5c34bb86ac
commit a0d5d61ba9
2 changed files with 942 additions and 27 deletions

View File

@@ -22,6 +22,12 @@
class="tab-item"
:class="{ active: activeTab === 'practice' }"
@click="switchTab('practice')">
<text>场景</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'training' }"
@click="switchTab('training')">
<text>陪练</text>
</view>
<view
@@ -106,6 +112,10 @@
class="practice-action-menu"
v-if="selectedPracticeItem && selectedPracticeItem.id === item.id && showPracticeActionMenu"
@tap.stop>
<view class="practice-action-menu-item" @tap.stop="handleStartPractice(item)">
<text>陪练</text>
</view>
<view class="practice-action-menu-divider"></view>
<view class="practice-action-menu-item" @tap.stop="handleEditPractice(item)">
<text>编辑</text>
</view>
@@ -279,6 +289,106 @@
</view>
</view>
<!-- 陪练列表内容 -->
<scroll-view
class="tab-content training-content"
scroll-y
v-if="activeTab === 'training'"
@scrolltolower="onTrainingReachBottom">
<view class="service-status-toolbar">
<text class="toolbar-total">{{ trainingTotal }}</text>
<input
class="toolbar-input"
v-model="trainingQuery.title"
placeholder="陪练标题"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onTrainingSearch"
/>
<input
class="toolbar-input"
v-model="trainingQuery.participantName"
placeholder="参与人姓名"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onTrainingSearch"
/>
<input
class="toolbar-input"
v-model="trainingQuery.participantPhone"
placeholder="参与人电话"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onTrainingSearch"
/>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--refresh" @click="onTrainingRefresh">
<uni-icons type="refresh" size="18" color="#2A68FF"></uni-icons>
<text>刷新</text>
</view>
</view>
</view>
<view
class="service-card training-card-item"
v-for="(item, index) in trainingList"
:key="item.id || index"
:class="{ 'training-card-item--active': selectedTrainingItem && selectedTrainingItem.id === item.id }">
<view class="card-header">
<view class="staff-info">
<view class="staff-avatar">
<text class="avatar-text">{{ (item.title || '陪').charAt(0) }}</text>
</view>
<text class="staff-name">{{ item.title || '未命名陪练' }}</text>
</view>
<view class="training-card-action-btn" @click.stop="onTrainingActionBtnClick(item)">
<uni-icons type="more-filled" size="20" color="#999"></uni-icons>
</view>
</view>
<!-- 操作菜单 -->
<view
class="training-action-menu"
v-if="selectedTrainingItem && selectedTrainingItem.id === item.id && showTrainingActionMenu"
@click.stop>
<view class="training-action-menu-item" @click="startTraining(item)">
<text>开始</text>
</view>
</view>
<view class="customer-info">
<text class="customer-name" v-if="item.scenario">陪练场景: {{ item.scenario }}</text>
</view>
<view class="customer-tags">
<view class="tag-item tag-blue" v-if="item.participantName">
<text>参与人: {{ item.participantName }}</text>
</view>
<view class="tag-item tag-orange" v-if="item.participantPhone">
<text>电话: {{ item.participantPhone }}</text>
</view>
</view>
<view class="service-duration" v-if="item.createTime">
<text>创建时间: {{ formatDateTime(item.createTime) }}</text>
</view>
<view class="service-duration" v-if="item.endTime">
<text>结束时间: {{ formatDateTime(item.endTime) }}</text>
</view>
</view>
<view class="service-status-empty" v-if="!trainingLoading && !trainingList.length">
<text>暂无陪练记录</text>
</view>
<view class="service-status-loading-more" v-if="trainingLoading && trainingList.length">
<text>正在加载更多...</text>
</view>
<!-- 遮罩层点击关闭菜单 -->
<view
class="training-action-menu-mask"
v-if="showTrainingActionMenu"
@click="closeTrainingActionMenu">
</view>
</scroll-view>
<!-- 质检内容 -->
<scroll-view
class="tab-content"
@@ -547,6 +657,116 @@
<text class="content-popup-text">{{ contentPopupText }}</text>
</scroll-view>
</view>
<!-- 陪练弹出框 -->
<view class="material-popup-mask" v-if="showPracticeTrainingPopup" @click="closePracticeTrainingPopup">
</view>
<view class="material-popup practice-training-popup" v-if="showPracticeTrainingPopup" @click.stop>
<view class="popup-header">
<text class="popup-title">生成陪练</text>
<view class="popup-close" @click="closePracticeTrainingPopup">
<uni-icons type="close" size="20" color="#999"></uni-icons>
</view>
</view>
<scroll-view class="popup-content" scroll-y>
<view class="form-card">
<view class="form-item">
<text class="form-item__label">陪练标题</text>
<input
class="form-item__input"
v-model="practiceTrainingForm.title"
placeholder="请输入陪练标题"
placeholder-style="color: #9ca3af"
maxlength="512"
/>
</view>
<view class="form-item">
<text class="form-item__label">陪练场景</text>
<input
class="form-item__input"
v-model="practiceTrainingForm.scenario"
placeholder="请输入陪练场景"
placeholder-style="color: #9ca3af"
maxlength="500"
:disabled="true"
readonly
/>
</view>
<view class="form-item">
<text class="form-item__label">参与人姓名</text>
<input
class="form-item__input"
v-model="practiceTrainingForm.participantName"
placeholder="请输入参与人姓名"
placeholder-style="color: #9ca3af"
maxlength="100"
/>
</view>
<view class="form-item">
<text class="form-item__label">参与人电话</text>
<input
class="form-item__input"
v-model="practiceTrainingForm.participantPhone"
placeholder="请输入参与人电话"
placeholder-style="color: #9ca3af"
type="number"
maxlength="20"
/>
</view>
</view>
</scroll-view>
<view class="popup-actions">
<view class="popup-btn popup-btn--cancel" @click="closePracticeTrainingPopup">
<uni-icons class="popup-btn__icon" type="close" size="20" color="#2A68FF"></uni-icons>
<text class="popup-btn__text">取消</text>
</view>
<view class="popup-btn popup-btn--submit" @click="submitPracticeTraining">
<uni-icons class="popup-btn__icon" type="checkmarkempty" size="20" color="#2A68FF"></uni-icons>
<text class="popup-btn__text">生成</text>
</view>
</view>
</view>
<!-- 陪练详情列表弹出框 -->
<view class="material-popup-mask" v-if="showTrainingItemListPopup" @click="closeTrainingItemListPopup">
</view>
<view class="material-popup training-item-list-popup" v-if="showTrainingItemListPopup" @click.stop>
<view class="popup-header">
<text class="popup-title">陪练详情</text>
<view class="popup-close" @click="closeTrainingItemListPopup">
<uni-icons type="close" size="20" color="#999"></uni-icons>
</view>
</view>
<scroll-view class="popup-content" scroll-y @scrolltolower="onTrainingItemListReachBottom">
<view
class="service-card training-item-card"
v-for="(item, index) in trainingItemList"
:key="item.id || index">
<view class="card-header">
<view class="staff-info">
<view class="staff-avatar">
<text class="avatar-text">{{ (item.question || '题').charAt(0) }}</text>
</view>
<text class="staff-name">{{ item.question || '未命名题目' }}</text>
</view>
</view>
<view class="customer-info" v-if="item.answer">
<text class="customer-name">答案: {{ item.answer }}</text>
</view>
<view class="service-duration" v-if="item.createTime">
<text>创建时间: {{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="service-status-empty" v-if="!trainingItemLoading && !trainingItemList.length">
<text>暂无详情记录</text>
</view>
<view class="service-status-loading-more" v-if="trainingItemLoading && trainingItemList.length">
<text>正在加载更多...</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
@@ -650,13 +870,52 @@
// 陪练操作菜单
selectedPracticeItem: null,
showPracticeActionMenu: false,
// 陪练弹出框
showPracticeTrainingPopup: false,
practiceTrainingForm: {
scenarioId: '',
title: '',
scenario: '',
participantName: '',
participantPhone: ''
},
// 陪练列表相关数据
trainingList: [],
trainingLoading: false,
trainingTotal: 0,
trainingPage: {
current: 1,
size: 10
},
trainingQuery: {
title: '',
participantName: '',
participantPhone: ''
},
// 内容详情弹出框
showContentPopup: false,
contentPopupTitle: '',
contentPopupText: ''
contentPopupText: '',
// 陪练操作菜单
selectedTrainingItem: null,
showTrainingActionMenu: false,
// 陪练详情列表弹出框
showTrainingItemListPopup: false,
trainingItemList: [],
trainingItemLoading: false,
trainingItemTotal: 0,
trainingItemPage: {
current: 1,
size: 10
},
currentTrainingParentId: ''
}
},
onLoad() {
// 页面加载时如果默认是场景tab页则调用接口填充数据
if (this.activeTab === 'practice') {
this.fetchPracticeList();
}
},
methods: {
goBack() {
@@ -668,8 +927,11 @@
// 当进入素材页面时,请求接口填充数据
this.fetchMaterialList();
} else if (tab === 'practice') {
// 当进入陪练页面时,请求接口填充数据
// 当进入场景页面时,请求接口填充数据
this.fetchPracticeList();
} else if (tab === 'training') {
// 当进入陪练页面时,请求接口填充数据
this.fetchTrainingList();
}
},
// 素材相关方法
@@ -1285,6 +1547,136 @@
this.showPracticeActionMenu = false;
this.selectedPracticeItem = null;
},
handleStartPractice(item) {
this.closePracticeActionMenu();
// 获取当前登录用户信息
let participantName = '';
let participantPhone = '';
try {
const loginResponse = uni.getStorageSync('backend-login-response') || {};
const phone = loginResponse.phone || '';
const userName = loginResponse.userName || '';
// 参与人电话默认为当前登录人的电话
participantPhone = phone;
// 参与人姓名默认为当前登录人的登录手机号或者登录账号(优先手机号)
participantName = phone || userName;
} catch (e) {
console.error('获取登录用户信息失败:', e);
}
// 填充陪练表单
this.practiceTrainingForm = {
scenarioId: item.id || '',
title: item.title || '',
scenario: item.content || item.detail || '',
participantName: participantName,
participantPhone: participantPhone
};
this.showPracticeTrainingPopup = true;
},
closePracticeTrainingPopup() {
this.showPracticeTrainingPopup = false;
this.practiceTrainingForm = {
scenarioId: '',
title: '',
scenario: '',
participantName: '',
participantPhone: ''
};
},
async submitPracticeTraining() {
// 表单验证
if (!this.practiceTrainingForm.title || !this.practiceTrainingForm.title.trim()) {
uni.showToast({
title: "请输入陪练标题",
icon: "none"
});
return;
}
if (!this.practiceTrainingForm.participantName || !this.practiceTrainingForm.participantName.trim()) {
uni.showToast({
title: "请输入参与人姓名",
icon: "none"
});
return;
}
if (!this.practiceTrainingForm.participantPhone || !this.practiceTrainingForm.participantPhone.trim()) {
uni.showToast({
title: "请输入参与人电话",
icon: "none"
});
return;
}
try {
uni.showLoading({
title: "创建中..."
});
// 获取租户ID
const tenantId = getTenantId();
const payload = {
scenarioId: this.practiceTrainingForm.scenarioId || '',
title: this.practiceTrainingForm.title?.trim() || '',
scenario: this.practiceTrainingForm.scenario?.trim() || '',
participantName: this.practiceTrainingForm.participantName.trim(),
participantPhone: this.practiceTrainingForm.participantPhone.trim()
};
// 添加租户ID到payload
if (tenantId) {
payload.tenantId = tenantId;
}
const headers = {
"Content-Type": "application/json"
};
// 添加租户ID到header
if (tenantId) {
headers['X-Tenant-Id'] = tenantId;
}
const res = await uni.request({
url: getApiUrl('/api/trainingMain/add'),
method: 'POST',
data: payload,
header: headers,
timeout: 15000
});
uni.hideLoading();
if (res.statusCode === 200 && res.data && (res.data.success || res.data.code === 200)) {
uni.showToast({
title: res.data?.message || "创建成功",
icon: "success"
});
// 关闭弹出框
this.closePracticeTrainingPopup();
// 跳转到陪练页面并刷新列表
this.switchTab('training');
this.trainingPage.current = 1;
this.fetchTrainingList({ force: true });
} else {
uni.showToast({
title: res.data?.message || "创建失败",
icon: "none"
});
}
} catch (error) {
uni.hideLoading();
console.error("创建陪练失败:", error);
uni.showToast({
title: error?.message || "创建失败,请重试",
icon: "none"
});
}
},
handleEditPractice(item) {
this.closePracticeActionMenu();
// 填充编辑表单
@@ -1569,6 +1961,280 @@
icon: "none"
});
}
},
// 陪练列表相关方法
onTrainingSearch() {
if (this.activeTab !== 'training') {
this.activeTab = 'training';
}
this.trainingPage.current = 1;
this.fetchTrainingList({ force: true });
},
onTrainingRefresh() {
this.trainingPage.current = 1;
this.fetchTrainingList({ force: true });
},
onTrainingReachBottom() {
// 已在加载中或全部加载完成则不再触发
if (this.trainingLoading) return;
if (this.trainingList.length >= this.trainingTotal) return;
this.trainingPage.current += 1;
this.fetchTrainingList();
},
async fetchTrainingList({ force = false } = {}) {
if (this.trainingLoading && !force) {
return;
}
this.trainingLoading = true;
try {
// 构建查询参数
const queryParams = {
current: this.trainingPage.current,
size: this.trainingPage.size
};
const trimmedTitle = this.trainingQuery?.title?.trim();
const trimmedParticipantName = this.trainingQuery?.participantName?.trim();
const trimmedParticipantPhone = this.trainingQuery?.participantPhone?.trim();
if (trimmedTitle) {
queryParams.title = trimmedTitle;
}
if (trimmedParticipantName) {
queryParams.participantName = trimmedParticipantName;
}
if (trimmedParticipantPhone) {
queryParams.participantPhone = trimmedParticipantPhone;
}
// 将参数转换为 URL 查询字符串
const queryString = Object.keys(queryParams)
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const url = `${getApiUrl('/api/trainingMain/list')}?${queryString}`;
// 获取租户ID
const tenantId = getTenantId();
const headers = {};
if (tenantId) {
headers['X-Tenant-Id'] = tenantId;
// 同时添加到查询参数中
queryParams.tenantId = tenantId;
}
// 重新构建查询字符串包含tenantId
const finalQueryString = Object.keys(queryParams)
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const finalUrl = `${getApiUrl('/api/trainingMain/list')}?${finalQueryString}`;
const res = await uni.request({
url: finalUrl,
method: 'POST',
header: headers,
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
const records = Array.isArray(res.data.data) ? res.data.data : [];
// 第一页或强制刷新时重置列表,否则追加
if (this.trainingPage.current === 1 || force) {
this.trainingList = records;
} else {
this.trainingList = this.trainingList.concat(records);
}
this.trainingTotal = Number(res.data.total) || 0;
} else {
this.trainingList = [];
this.trainingTotal = 0;
uni.showToast({
title: res.data?.message || '获取陪练列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取陪练列表失败:', error);
uni.showToast({
title: '获取陪练列表失败,请稍后重试',
icon: 'none'
});
} finally {
this.trainingLoading = false;
}
},
// 陪练操作菜单相关方法
onTrainingActionBtnClick(item) {
// 如果已经选中当前项,则关闭菜单
if (this.selectedTrainingItem && this.selectedTrainingItem.id === item.id && this.showTrainingActionMenu) {
this.closeTrainingActionMenu();
return;
}
// 显示菜单
this.selectedTrainingItem = item;
this.showTrainingActionMenu = true;
},
closeTrainingActionMenu() {
this.showTrainingActionMenu = false;
this.selectedTrainingItem = null;
},
async startTraining(item) {
this.closeTrainingActionMenu();
// 打开详情列表弹窗
this.currentTrainingParentId = item.id || '';
this.trainingItemList = [];
this.showTrainingItemListPopup = true;
// 加载详情列表调用listAll接口
await this.fetchTrainingItemListAll(item.id || '');
},
closeTrainingItemListPopup() {
this.showTrainingItemListPopup = false;
this.currentTrainingParentId = '';
this.trainingItemPage.current = 1;
this.trainingItemList = [];
},
onTrainingItemListReachBottom() {
// 已在加载中或全部加载完成则不再触发
if (this.trainingItemLoading) return;
if (this.trainingItemList.length >= this.trainingItemTotal) return;
this.trainingItemPage.current += 1;
this.fetchTrainingItemList();
},
async fetchTrainingItemListAll(parentId) {
if (!parentId) {
uni.showToast({
title: '缺少父ID参数',
icon: 'none'
});
return;
}
this.trainingItemLoading = true;
try {
// 获取租户ID
const tenantId = getTenantId();
// 构建查询参数
const queryParams = {
parentId: parentId
};
if (tenantId) {
queryParams.tenantId = tenantId;
}
// 将参数转换为 URL 查询字符串
const queryString = Object.keys(queryParams)
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const url = `${getApiUrl('/api/trainingItem/listAll')}?${queryString}`;
// 构建请求头
const headers = {
"Content-Type": "application/json"
};
if (tenantId) {
headers['X-Tenant-Id'] = tenantId;
}
const res = await uni.request({
url: url,
method: 'POST',
header: headers,
timeout: 30000
});
if (res.statusCode === 200 && res.data && res.data.success) {
const records = Array.isArray(res.data.data) ? res.data.data : [];
this.trainingItemList = records;
this.trainingItemTotal = Number(res.data.count) || records.length;
} else {
this.trainingItemList = [];
this.trainingItemTotal = 0;
uni.showToast({
title: res.data?.message || '获取陪练详情列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取陪练详情列表失败:', error);
uni.showToast({
title: '获取陪练详情列表失败,请稍后重试',
icon: 'none'
});
} finally {
this.trainingItemLoading = false;
}
},
async fetchTrainingItemList({ force = false } = {}) {
if (this.trainingItemLoading && !force) {
return;
}
if (!this.currentTrainingParentId) {
return;
}
this.trainingItemLoading = true;
try {
// 构建查询参数
const queryParams = {
current: this.trainingItemPage.current,
size: this.trainingItemPage.size,
parentId: this.currentTrainingParentId
};
// 获取租户ID
const tenantId = getTenantId();
if (tenantId) {
queryParams.tenantId = tenantId;
}
// 将参数转换为 URL 查询字符串
const queryString = Object.keys(queryParams)
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const url = `${getApiUrl('/api/trainingItem/list')}?${queryString}`;
// 构建请求头
const headers = {};
if (tenantId) {
headers['X-Tenant-Id'] = tenantId;
}
const res = await uni.request({
url: url,
method: 'GET',
header: headers,
timeout: 30000
});
if (res.statusCode === 200 && res.data && res.data.success) {
const records = Array.isArray(res.data.data) ? res.data.data : [];
// 第一页或强制刷新时重置列表,否则追加
if (this.trainingItemPage.current === 1 || force) {
this.trainingItemList = records;
} else {
this.trainingItemList = this.trainingItemList.concat(records);
}
this.trainingItemTotal = Number(res.data.total) || 0;
} else {
this.trainingItemList = [];
this.trainingItemTotal = 0;
uni.showToast({
title: res.data?.message || '获取陪练详情列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取陪练详情列表失败:', error);
uni.showToast({
title: '获取陪练详情列表失败,请稍后重试',
icon: 'none'
});
} finally {
this.trainingItemLoading = false;
}
}
}
}
@@ -1652,7 +2318,8 @@
/* 素材内容区域和陪练内容区域使用相同定位 */
.material-content,
.practice-content {
.practice-content,
.training-content {
position: absolute;
top: 160rpx; /* 导航栏(88rpx) + tab页(72rpx) = 160rpx */
left: 0;
@@ -1769,7 +2436,8 @@
overflow: visible;
}
.material-card-item {
.material-card-item,
.training-card-item {
padding-left: 32rpx;
padding-right: 32rpx;
padding-top: 32rpx;
@@ -2302,5 +2970,90 @@
white-space: pre-wrap;
word-wrap: break-word;
}
/* 陪练操作菜单样式 */
.training-card-item {
position: relative;
}
.training-card-action-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: auto;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
flex-shrink: 0;
flex-grow: 0;
position: relative;
padding: 0;
}
.training-card-action-btn uni-icons {
display: block;
margin: 0;
padding: 0;
line-height: 1;
}
.training-card-action-btn:active {
opacity: 0.7;
}
.training-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;
}
.training-action-menu-item {
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #333;
text-align: center;
background-color: #FFFFFF;
}
.training-action-menu-item:active {
background-color: #F5F5F5;
}
.training-action-menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 99;
}
/* 陪练详情列表弹窗样式 */
.training-item-list-popup {
max-height: 80vh;
}
.training-item-list-popup .popup-content {
padding: 24rpx;
}
.training-item-card {
margin-bottom: 24rpx;
}
.training-item-card:last-child {
margin-bottom: 0;
}
</style>