feat(mobile): align employee practice screens for release

This commit is contained in:
2026-07-15 11:23:08 +08:00
parent 3541aab3fe
commit eacf1a0aea
16 changed files with 307 additions and 191 deletions
+103 -63
View File
@@ -1,8 +1,8 @@
<template>
<view class="page practice-page employee-page-shell">
<view class="practice-head">
<text class="page-title">训练前预习</text>
<text class="page-subtitle">先掌握要点,再开始场景练习</text>
<text class="page-title">{{ practicePageTitle }}</text>
<text class="page-subtitle">{{ practicePageSubtitle }}</text>
</view>
<view v-if="message" class="status-message">{{ message }}</view>
@@ -15,7 +15,7 @@
<button class="primary-button login-button" @click="goLogin">去登录</button>
</view>
<view v-if="loggedIn" class="panel scenario-card employee-card">
<view v-if="loggedIn && practiceView === 'prep'" class="panel scenario-card employee-card">
<view class="scenario-headline">
<text class="scenario-title">{{ activeScenarioName }}</text>
<text class="scenario-desc">{{ positionLabel }} · 业主服务场景</text>
@@ -53,7 +53,7 @@
</button>
</view>
<view v-if="loggedIn" class="panel assignment-panel recent-panel">
<view v-if="loggedIn && practiceView === 'prep'" class="panel assignment-panel recent-panel">
<view class="assignment-head">
<text class="panel-title">最近练过</text>
<text class="recent-link">全部 ›</text>
@@ -74,7 +74,7 @@
<view v-else class="empty">暂无最近练习</view>
</view>
<view v-if="loggedIn" class="panel assignment-panel daily-panel">
<view v-if="loggedIn && practiceView === 'prep'" class="panel assignment-panel daily-panel">
<view class="assignment-head">
<view><text class="panel-title">每日三题</text><text class="daily-subtitle">今日安排</text></view>
<text class="daily-action-label">去作答 ›</text>
@@ -123,13 +123,13 @@
</view>
</view>
<view v-if="loggedIn" class="capability-strip">
<view v-if="loggedIn && practiceView === 'prep'" class="capability-strip">
<view><text>情绪</text><text>识别与安抚</text></view>
<view><text>信任</text><text>建立与维护</text></view>
<view><text>红线纠偏</text><text>识别与规避</text></view>
</view>
<view v-if="turns.length" class="panel dialogue-card employee-card">
<view v-if="practiceView === 'active' && turns.length" class="panel dialogue-card employee-card">
<view class="dialogue-head">
<view class="dialogue-heading-block">
<text class="panel-title">对话</text>
@@ -197,7 +197,7 @@
<text v-if="draftAudioUrl" class="voice-state">已附带语音回放</text>
</view>
<view v-if="helpAnswer || helpRefs.length" class="panel help-card employee-card">
<view v-if="practiceView === 'active' && (helpAnswer || helpRefs.length)" class="panel help-card employee-card">
<text class="panel-title">SOP 提示</text>
<text v-if="helpAnswer" class="help-answer">{{ helpAnswer }}</text>
<view v-if="helpRefs.length" class="help-refs">
@@ -248,6 +248,11 @@
</button>
<text v-else class="satisfaction-thanks">感谢反馈,已记录本次训练体验。</text>
</view>
<view v-if="result" class="result-actions">
<button class="secondary-button" @click="openPracticeHistory">查看训练记录</button>
<button class="primary-button" @click="start(selectedScenarioId)">再练一次</button>
</view>
</view>
</template>
@@ -327,6 +332,19 @@ const draftAudioOssId = ref<number | undefined>();
const mediaRecorder = ref<MediaRecorder | null>(null);
const mediaStream = ref<MediaStream | null>(null);
const busy = computed(() => status.value === 'starting' || status.value === 'submitting');
const practiceView = computed<'prep' | 'active' | 'result'>(() => {
if (result.value) return 'result';
return status.value === 'active' || status.value === 'submitting' ? 'active' : 'prep';
});
const practicePageTitle = computed(() => {
if (practiceView.value === 'result') return '训练完成';
return practiceView.value === 'active' ? '场景对练' : '训练前预习';
});
const practicePageSubtitle = computed(() => {
if (practiceView.value === 'result') return `${activeScenarioName.value} · ${Math.max(roundIndex.value, 1)} 轮对话`;
if (practiceView.value === 'active') return `${activeScenarioName.value} · 第 ${Math.min(roundIndex.value + 1, 3)}/3 轮`;
return '先掌握要点,再开始场景练习';
});
const canStartPractice = computed(() => status.value === 'idle' || status.value === 'finished');
const hasTraineeReply = computed(() => turns.value.some((item) => item.role === 'trainee'));
const canFinishPractice = computed(() => hasTraineeReply.value && !redFlagActive.value);
@@ -643,6 +661,10 @@ const openRecentItem = (item: RecentPracticeItem) => {
uni.navigateTo({ url: '/pages/user/profile/index' });
};
const openPracticeHistory = () => {
uni.navigateTo({ url: '/pages/user/profile/index' });
};
const consumePendingAssignment = () => {
const raw = uni.getStorageSync(pendingAssignmentKey);
if (!raw) return;
@@ -1244,7 +1266,7 @@ onShow(() => {
}
.dialogue-card {
border-color: #dbe7ff;
border-color: #f3dadd;
box-shadow: var(--employee-card-shadow);
}
@@ -1306,11 +1328,11 @@ onShow(() => {
}
.emotion-fill {
background: #6f9cff;
background: #e60012;
}
.trust-fill {
background: #2f6bff;
background: #8c929a;
}
.red-flag {
@@ -1348,19 +1370,19 @@ onShow(() => {
}
.turn-row.trainee {
border-left-color: #2f6bff;
background: #eef4ff;
border-left-color: #e60012;
background: #fff0f1;
}
.turn-row.coach {
border-left-color: #8bb1ff;
background: #f4f7ff;
border-left-color: #ef8992;
background: #fff7f7;
}
.reply-actions button.recording-active {
border-color: #2f6bff;
background: #eef4ff;
color: #2f6bff;
border-color: #e60012;
background: #fff0f1;
color: #e60012;
}
.turn-role {
@@ -1384,9 +1406,9 @@ onShow(() => {
.style-chip {
width: auto;
border-color: #c9d9ff;
border-color: #f0c7cb;
background: #ffffff;
color: #2f6bff;
color: #e60012;
}
.reply-card {
@@ -1396,7 +1418,7 @@ onShow(() => {
.practice-textarea {
min-height: 124px;
border-color: #dbe7ff;
border-color: #f3dadd;
border-radius: 18px;
background: #f9fbff;
}
@@ -1416,8 +1438,8 @@ onShow(() => {
.help-ref {
padding: 5px 8px;
border-radius: 5px;
background: #eef4ff;
color: #2f6bff;
background: #fff0f1;
color: #e60012;
font-size: 12px;
font-weight: 800;
}
@@ -1429,7 +1451,7 @@ onShow(() => {
}
.result-score {
color: #2f6bff;
color: #e60012;
font-size: 44px;
font-weight: 900;
line-height: 1;
@@ -1447,13 +1469,13 @@ onShow(() => {
flex-direction: column;
gap: 6px;
padding: 12px;
border-left: 3px solid #2f6bff;
border-left: 3px solid #e60012;
border-radius: 6px;
background: #eef4ff;
background: #fff0f1;
}
.mentor-rewrite text:first-child {
color: #2f6bff;
color: #e60012;
font-weight: 900;
}
@@ -1481,9 +1503,9 @@ onShow(() => {
}
.satisfaction-option.selected {
border-color: #2f6bff;
background: #eef4ff;
color: #2f6bff;
border-color: #e60012;
background: #fff0f1;
color: #e60012;
}
.satisfaction-textarea {
@@ -1496,6 +1518,18 @@ onShow(() => {
font-weight: 800;
}
.result-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 14px;
}
.result-actions button {
width: 100%;
min-width: 0;
}
/* 练页入口态:与员工端蓝白轻卡片体系保持一致 */
.practice-page {
padding: 0 16px 108px;
@@ -1504,16 +1538,16 @@ onShow(() => {
.practice-head {
margin: 0 -16px;
padding: 30px 20px 24px;
border-bottom: 1px solid var(--employee-line);
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
text-align: left;
padding: 28px 20px 16px;
border-bottom: 0;
background: #f7f7f7;
text-align: center;
}
.practice-head::after,
.practice-eyebrow,
.practice-tip {
display: block;
display: none;
}
.practice-head .page-title {
@@ -1665,9 +1699,9 @@ onShow(() => {
}
.scenario-card .primary-button {
margin-top: 18px;
background: linear-gradient(135deg, #2f6bff, #5a8cff);
box-shadow: 0 8px 16px rgba(47, 107, 255, 0.18);
margin-top: 2px;
background: var(--employee-primary);
box-shadow: 0 8px 16px rgba(230, 0, 18, 0.18);
}
.login-panel {
@@ -1675,12 +1709,12 @@ onShow(() => {
}
.assignment-row {
border-color: #dbe7ff;
border-color: #f3dadd;
}
.assignment-row.highlighted {
border-color: #9bbaff;
background: #f4f7ff;
border-color: #ef8992;
background: #fff7f7;
}
.assignment-badge,
@@ -1803,8 +1837,8 @@ onShow(() => {
}
.daily-row .small-button {
background: linear-gradient(135deg, #2f6bff, #5a8cff);
box-shadow: 0 6px 12px rgba(47, 107, 255, 0.16);
background: linear-gradient(135deg, #ed1f28, #ff4b4d);
box-shadow: 0 6px 12px rgba(230, 0, 18, 0.16);
}
.capability-strip {
@@ -1843,7 +1877,7 @@ onShow(() => {
.practice-head .page-title {
color: #1f2024;
font-size: 29px;
font-size: 30px;
letter-spacing: 0.5px;
}
@@ -1852,36 +1886,43 @@ onShow(() => {
}
.scenario-card {
padding: 20px 16px 16px;
border-radius: 24px;
background: #ffffff;
box-shadow: 0 10px 26px rgba(24, 28, 36, 0.06);
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.scenario-headline {
max-width: none;
min-height: 42px;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 0 14px;
border: 1px solid #ffd1d5;
border-radius: 22px;
background: #fff7f7;
}
.scenario-title {
color: #25282d;
font-size: 27px;
color: var(--employee-primary);
font-size: 14px;
}
.scenario-desc {
color: #8a8d93;
font-size: 14px;
color: var(--employee-primary);
font-size: 13px;
}
.scenario-persona {
display: flex;
align-items: center;
gap: 12px;
margin-top: 2px;
margin-top: 14px;
padding: 14px;
border: 1px solid #ffd9dc;
border-radius: 16px;
background: #fff8f8;
border: 0;
border-radius: 20px;
background: #ffffff;
}
.persona-avatar {
@@ -1930,15 +1971,15 @@ onShow(() => {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 2px;
margin-top: 14px;
}
.prep-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border: 1px solid #f0f1f3;
gap: 7px;
padding: 15px 16px;
border: 0;
border-radius: 20px;
background: #ffffff;
}
@@ -1950,8 +1991,7 @@ onShow(() => {
}
.prep-section-redline {
border-color: #ffe0e2;
background: #fffafa;
background: #ffffff;
}
.prep-section-redline .prep-section-title,
@@ -1964,7 +2004,7 @@ onShow(() => {
margin-top: 0;
color: #858990;
font-size: 13px;
line-height: 1.5;
line-height: 1.42;
}
.prep-mark {