fix(mobile): bottom tabbar switches whole views instead of inline panels
用户反馈「底部菜单不像正常菜单切换,点击后只在当前页面」修复: tab 视图互斥渲染(isHomeTab + activeUserPanel/activeSupervisorPanel 条件重组),三端首页/训练/知识/我的整页切换,不再页中叠加;切 tab 滚动回顶;训练 tab 不自动开局、进行中会话切走可切回;SOP 全屏层 返回进入前 tab;首页「我的成长」收敛为摘要卡(综合能力/今日完成/ 待复盘+查看全部),完整画像与历史仅在「我的」tab,消除同屏重复。
This commit is contained in:
+184
-60
@@ -52,7 +52,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Card v-if="roleKey !== 'user'" :title="roleKey === 'supervisor' ? '今日处理流程' : '今日使用流程'">
|
<Card v-if="roleKey !== 'user' && isHomeTab" :title="roleKey === 'supervisor' ? '今日处理流程' : '今日使用流程'">
|
||||||
<div class="flow-steps">
|
<div class="flow-steps">
|
||||||
<button v-for="item in roleFlowSteps" :key="`${item.phase}-${item.title}`" type="button" :class="{ active: isFlowActive(item) }" @click="openFlowStep(item)">
|
<button v-for="item in roleFlowSteps" :key="`${item.phase}-${item.title}`" type="button" :class="{ active: isFlowActive(item) }" @click="openFlowStep(item)">
|
||||||
<em>{{ item.phase }}</em>
|
<em>{{ item.phase }}</em>
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<template v-if="roleKey === 'supervisor'">
|
<template v-if="roleKey === 'supervisor'">
|
||||||
|
<template v-if="isHomeTab">
|
||||||
<Card title="团队今日概况">
|
<Card title="团队今日概况">
|
||||||
<div class="team-card">
|
<div class="team-card">
|
||||||
<div class="team-copy">
|
<div class="team-copy">
|
||||||
@@ -169,8 +170,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<Icon name="chevron-right" size="26" />
|
<Icon name="chevron-right" size="26" />
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<Card v-if="activeSupervisorPanel" :title="supervisorPanelTitle">
|
<Card v-if="activeSupervisorPanel && !isHomeTab" :title="supervisorPanelTitle">
|
||||||
<div class="user-panel">
|
<div class="user-panel">
|
||||||
<template v-if="activeSupervisorPanel === 'team' || activeSupervisorPanel === 'weakness'">
|
<template v-if="activeSupervisorPanel === 'team' || activeSupervisorPanel === 'weakness'">
|
||||||
<div class="progress-grid">
|
<div class="progress-grid">
|
||||||
@@ -201,6 +203,42 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<em v-else class="panel-message">暂无待复盘记录</em>
|
<em v-else class="panel-message">暂无待复盘记录</em>
|
||||||
|
<div v-if="selectedReview" class="review-detail">
|
||||||
|
<div class="review-detail-head">
|
||||||
|
<span>
|
||||||
|
<b>{{ selectedReview.trainee }} · {{ selectedReview.score }}分</b>
|
||||||
|
<small>{{ selectedReview.time }} · {{ selectedReview.scene }}</small>
|
||||||
|
</span>
|
||||||
|
<em>{{ selectedReview.status }}</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="review-score-grid">
|
||||||
|
<p v-for="item in selectedReview.scoreItems" :key="item.label">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<b>{{ item.value }}</b>
|
||||||
|
<small>{{ item.note }}</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="practice-dialogue review-dialogue">
|
||||||
|
<p v-for="(turn, index) in selectedReview.dialogue" :key="`${turn.role}-${index}`" :class="turn.role">
|
||||||
|
<span>{{ turn.label }}</span>
|
||||||
|
{{ turn.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="review-summary">
|
||||||
|
<b>导师改写</b>
|
||||||
|
<p>{{ selectedReview.mentorRewrite }}</p>
|
||||||
|
<b>AI点评</b>
|
||||||
|
<p>{{ selectedReview.aiComment }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" :disabled="markingReviewed || selectedReview.status !== '待复盘'" @click="markReviewDone">
|
||||||
|
{{ selectedReview.status === '待复盘' ? (markingReviewed ? '处理中' : '标记已复盘') : '已复盘' }}
|
||||||
|
</button>
|
||||||
|
<em v-if="reviewMessage">{{ reviewMessage }}</em>
|
||||||
|
</div>
|
||||||
<div class="result-list">
|
<div class="result-list">
|
||||||
<h3>专项建议</h3>
|
<h3>专项建议</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -270,15 +308,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="panel-actions">
|
|
||||||
<button type="button" @click="activeSupervisorPanel = ''">收起</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="roleKey === 'user'">
|
<template v-if="roleKey === 'user' && isHomeTab">
|
||||||
<section class="scenario-section">
|
<section class="scenario-section">
|
||||||
<div class="scenario-title">
|
<div class="scenario-title">
|
||||||
<h2>今天要学</h2>
|
<h2>今天要学</h2>
|
||||||
@@ -406,7 +441,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else-if="isHomeTab">
|
||||||
<Card title="今日任务" action="查看全部" @action="openUserAction('tasks')">
|
<Card title="今日任务" action="查看全部" @action="openUserAction('tasks')">
|
||||||
<div class="task-card">
|
<div class="task-card">
|
||||||
<div class="task-copy">
|
<div class="task-copy">
|
||||||
@@ -437,9 +472,75 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Card v-if="roleKey !== 'supervisor' && activeUserPanel" :title="userPanelTitle">
|
<Card v-if="roleKey !== 'supervisor' && activeUserPanel && !isHomeTab" :title="userPanelTitle">
|
||||||
<div class="user-panel">
|
<div class="user-panel">
|
||||||
<template v-if="activeUserPanel === 'tasks'">
|
<template v-if="activeUserPanel === 'tasks'">
|
||||||
|
<template v-if="roleKey === 'user'">
|
||||||
|
<div class="task-card">
|
||||||
|
<div class="task-copy">
|
||||||
|
<strong>{{ userPrimaryTask.badge }}</strong>
|
||||||
|
<h3>{{ userPrimaryTask.title }}</h3>
|
||||||
|
<p>{{ userPrimaryTask.desc }}</p>
|
||||||
|
<span>进度 {{ userPrimaryTask.progressText }}</span>
|
||||||
|
<ProgressBar :value="userPrimaryTask.progress" />
|
||||||
|
</div>
|
||||||
|
<div class="headset-art">
|
||||||
|
<Icon :name="userPrimaryTask.icon" size="104" stroke="1.7" />
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="handlePrimaryAction">{{ userPrimaryTask.cta }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="practiceStatus !== 'idle'" class="practice-panel">
|
||||||
|
<div class="practice-panel-head">
|
||||||
|
<span>三角色对练</span>
|
||||||
|
<b v-if="practiceResult">{{ practiceResult.totalScore }}分</b>
|
||||||
|
<small v-else>{{ practiceStatus === 'starting' ? '准备中' : '进行中' }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="practiceResult" class="practice-result">
|
||||||
|
<strong>{{ practiceResult.aiComment }}</strong>
|
||||||
|
<p>{{ practiceResult.mentorRewrite }}</p>
|
||||||
|
<div class="practice-result-actions">
|
||||||
|
<button type="button" @click="startMobilePractice">再练一次</button>
|
||||||
|
<button type="button" @click="goSupervisor">查看主管端</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="practice-live">
|
||||||
|
<div class="practice-dialogue">
|
||||||
|
<p v-for="(turn, index) in practiceTurns" :key="`${turn.role}-${index}`" :class="turn.role">
|
||||||
|
<span>{{ turn.label }}</span>
|
||||||
|
{{ turn.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<textarea v-model.trim="practiceDraft" rows="3" placeholder="输入你的回复" />
|
||||||
|
<div class="practice-actions">
|
||||||
|
<button type="button" :disabled="asrBusy" @click="toggleRecording">
|
||||||
|
{{ recording ? '停止录音' : asrBusy ? '识别中' : useAudioFileFallback ? '选择语音' : '语音输入' }}
|
||||||
|
</button>
|
||||||
|
<input ref="practiceAudioInput" class="practice-audio-input" type="file" accept="audio/*,.mp3,.wav,.m4a,.webm,.ogg" capture @change="handlePracticeAudioFile" />
|
||||||
|
<button type="button" @click="toggleVoice">{{ voiceEnabled ? '播报:开' : '播报:关' }}</button>
|
||||||
|
<button type="button" @click="fillPracticeReply">填入建议回复</button>
|
||||||
|
<button type="button" :disabled="practiceStatus === 'starting' || practiceStatus === 'submitting'" @click="submitMobilePractice">
|
||||||
|
{{ practiceStatus === 'submitting' ? '提交中' : '提交本轮' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<em v-if="practiceMessage">{{ practiceMessage }}</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="practiceHistory.length" class="record-list">
|
||||||
|
<h3>训练历史</h3>
|
||||||
|
<button v-for="item in practiceHistory" :key="`${item.time}-${item.scene}`" type="button" @click="openUserHistory(item)">
|
||||||
|
<span>
|
||||||
|
<b>{{ item.scene }} · {{ item.score }}分</b>
|
||||||
|
<small>{{ item.time }} · {{ item.summary }}</small>
|
||||||
|
</span>
|
||||||
|
<em>{{ item.status }}</em>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<button class="panel-row" type="button" @click="handlePrimaryAction">
|
<button class="panel-row" type="button" @click="handlePrimaryAction">
|
||||||
<IconBubble :icon="workerRole.primary.icon" tone="teal" />
|
<IconBubble :icon="workerRole.primary.icon" tone="teal" />
|
||||||
<span>
|
<span>
|
||||||
@@ -507,9 +608,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<em v-if="interviewMessage">{{ interviewMessage }}</em>
|
<em v-if="interviewMessage">{{ interviewMessage }}</em>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeUserPanel === 'sop' || activeUserPanel === 'cases'">
|
<template v-else-if="activeUserPanel === 'sop' || activeUserPanel === 'cases'">
|
||||||
|
<div class="knowledge-tabs">
|
||||||
|
<button type="button" :class="{ active: activeUserPanel === 'sop' }" @click="switchKnowledgePanel('sop')">SOP</button>
|
||||||
|
<button type="button" :class="{ active: activeUserPanel === 'cases' }" @click="switchKnowledgePanel('cases')">案例</button>
|
||||||
|
</div>
|
||||||
<form class="knowledge-search" @submit.prevent="searchKnowledge()">
|
<form class="knowledge-search" @submit.prevent="searchKnowledge()">
|
||||||
<input v-model.trim="knowledgeQuery" :placeholder="activeUserPanel === 'cases' ? '搜索案例关键词' : '搜索SOP关键词'" />
|
<input v-model.trim="knowledgeQuery" :placeholder="activeUserPanel === 'cases' ? '搜索案例关键词' : '搜索SOP关键词'" />
|
||||||
<button type="submit" :disabled="knowledgeLoading">{{ knowledgeLoading ? '查询中' : '查询' }}</button>
|
<button type="submit" :disabled="knowledgeLoading">{{ knowledgeLoading ? '查询中' : '查询' }}</button>
|
||||||
@@ -687,59 +793,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="panel-actions">
|
|
||||||
<button type="button" @click="activeUserPanel = ''">收起</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<section v-if="roleKey === 'user'" class="scenario-section growth-section">
|
<section v-if="roleKey === 'user' && isHomeTab" class="scenario-section growth-section">
|
||||||
<div class="scenario-title">
|
<div class="scenario-title">
|
||||||
<h2>我的成长</h2>
|
<h2>我的成长</h2>
|
||||||
<p>训练、复盘和能力画像集中在这里看</p>
|
<p>这里只看摘要,完整记录去我的页</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card title="我的进度" :action="workerRole.recommend" compact-action @action="openUserAction('recommend')">
|
<Card title="我的进度" action="查看全部" compact-action @action="openUserAction('profile')">
|
||||||
<div class="progress-grid">
|
<div class="profile-card growth-summary">
|
||||||
<ProgressTile v-for="item in workerRole.metrics" :key="item.title" :item="item" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="competencyProfile" class="profile-card">
|
|
||||||
<span>
|
<span>
|
||||||
<small>综合能力</small>
|
<small>综合能力</small>
|
||||||
<b>{{ competencyProfile.score }}</b>
|
<b>{{ competencyProfile?.score || workerRole.metrics[1]?.value || '-' }}</b>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<small>训练历史</small>
|
<small>今日完成</small>
|
||||||
<b>{{ competencyProfile.completed }}</b>
|
<b>{{ todayPracticeCompleted }}</b>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<small>待复盘</small>
|
<small>待复盘</small>
|
||||||
<b>{{ competencyProfile.pendingReview }}</b>
|
<b>{{ competencyProfile?.pendingReview ?? workerRole.metrics[2]?.value ?? '-' }}</b>
|
||||||
</span>
|
</span>
|
||||||
<div class="profile-dims">
|
|
||||||
<p v-for="item in competencyProfile.dimensions" :key="item.label">
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
<b>{{ item.value }}</b>
|
|
||||||
<small>{{ item.note }}</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="practiceHistory.length" class="record-list">
|
|
||||||
<h3>训练历史</h3>
|
|
||||||
<button v-for="item in practiceHistory" :key="`${item.time}-${item.scene}`" type="button" @click="openUserHistory(item)">
|
|
||||||
<span>
|
|
||||||
<b>{{ item.scene }} · {{ item.score }}分</b>
|
|
||||||
<small>{{ item.time }} · {{ item.summary }}</small>
|
|
||||||
</span>
|
|
||||||
<em>{{ item.status }}</em>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Card v-else title="我的进度" :action="workerRole.recommend" compact-action @action="openUserAction('recommend')">
|
<Card v-else-if="roleKey === 'candidate' && isHomeTab" title="我的进度" :action="workerRole.recommend" compact-action @action="openUserAction('recommend')">
|
||||||
<div class="progress-grid">
|
<div class="progress-grid">
|
||||||
<ProgressTile v-for="item in workerRole.metrics" :key="item.title" :item="item" />
|
<ProgressTile v-for="item in workerRole.metrics" :key="item.title" :item="item" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1168,6 +1249,7 @@ const phonePattern = /^1[3-9]\d{9}$/;
|
|||||||
|
|
||||||
const roleKey = ref<RoleKey>(readRole());
|
const roleKey = ref<RoleKey>(readRole());
|
||||||
const activeTabLabel = ref('首页');
|
const activeTabLabel = ref('首页');
|
||||||
|
const isHomeTab = computed(() => activeTabLabel.value === '首页');
|
||||||
const tapped = ref('');
|
const tapped = ref('');
|
||||||
const toastMessage = ref('');
|
const toastMessage = ref('');
|
||||||
const phone = ref(localStorage.getItem(phoneKey) || '');
|
const phone = ref(localStorage.getItem(phoneKey) || '');
|
||||||
@@ -1206,6 +1288,7 @@ const knowledgeResult = ref<KnowledgeSearchResponse | null>(null);
|
|||||||
const knowledgeLoading = ref(false);
|
const knowledgeLoading = ref(false);
|
||||||
const knowledgeMessage = ref('');
|
const knowledgeMessage = ref('');
|
||||||
const sopPanelOpen = ref(false);
|
const sopPanelOpen = ref(false);
|
||||||
|
const sopReturnTabLabel = ref('首页');
|
||||||
const sopQuery = ref('');
|
const sopQuery = ref('');
|
||||||
const sopResult = ref<KnowledgeSearchResponse | null>(null);
|
const sopResult = ref<KnowledgeSearchResponse | null>(null);
|
||||||
const sopLoading = ref(false);
|
const sopLoading = ref(false);
|
||||||
@@ -1232,6 +1315,9 @@ let sopRecordedChunks: Blob[] = [];
|
|||||||
let customerAudio: HTMLAudioElement | null = null;
|
let customerAudio: HTMLAudioElement | null = null;
|
||||||
|
|
||||||
const syncPath = () => window.history.pushState({}, '', rolePaths[roleKey.value]);
|
const syncPath = () => window.history.pushState({}, '', rolePaths[roleKey.value]);
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.requestAnimationFrame(() => window.scrollTo({ top: 0 }));
|
||||||
|
};
|
||||||
const tap = (name: string) => {
|
const tap = (name: string) => {
|
||||||
tapped.value = name;
|
tapped.value = name;
|
||||||
toastMessage.value = `「${name}」本期演示未开放,正在建设中`;
|
toastMessage.value = `「${name}」本期演示未开放,正在建设中`;
|
||||||
@@ -1434,9 +1520,7 @@ const transcribeRecording = async (blob: Blob, filename = 'practice.webm') => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openSopPanel = () => {
|
const openSopPanel = () => {
|
||||||
activeTabLabel.value = roleKey.value === 'supervisor' ? '案例' : '知识';
|
sopReturnTabLabel.value = activeTabLabel.value;
|
||||||
activeUserPanel.value = '';
|
|
||||||
activeSupervisorPanel.value = '';
|
|
||||||
sopPanelOpen.value = true;
|
sopPanelOpen.value = true;
|
||||||
sopQuery.value = '';
|
sopQuery.value = '';
|
||||||
sopResult.value = null;
|
sopResult.value = null;
|
||||||
@@ -1448,6 +1532,7 @@ const closeSopPanel = () => {
|
|||||||
sopMediaRecorder?.stop();
|
sopMediaRecorder?.stop();
|
||||||
}
|
}
|
||||||
sopPanelOpen.value = false;
|
sopPanelOpen.value = false;
|
||||||
|
activeTabLabel.value = sopReturnTabLabel.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSopRecording = async () => {
|
const toggleSopRecording = async () => {
|
||||||
@@ -1578,6 +1663,14 @@ const searchKnowledge = async (query = knowledgeQuery.value, category = activeUs
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const switchKnowledgePanel = (panel: Extract<UserPanelKey, 'sop' | 'cases'>) => {
|
||||||
|
activeTabLabel.value = '知识';
|
||||||
|
activeUserPanel.value = panel;
|
||||||
|
knowledgeQuery.value =
|
||||||
|
panel === 'cases' ? '投诉接待优秀案例' : roleKey.value === 'candidate' ? '客服接待基础流程' : '住宅投诉处理流程';
|
||||||
|
void searchKnowledge(knowledgeQuery.value, panel === 'cases' ? 'case' : 'sop');
|
||||||
|
};
|
||||||
|
|
||||||
const openFlowStep = (item: FlowStep) => {
|
const openFlowStep = (item: FlowStep) => {
|
||||||
if (roleKey.value === 'supervisor') {
|
if (roleKey.value === 'supervisor') {
|
||||||
openSupervisorAction(item.target as SupervisorPanelKey);
|
openSupervisorAction(item.target as SupervisorPanelKey);
|
||||||
@@ -1601,6 +1694,7 @@ const openCandidateAction = (panel: UserPanelKey) => {
|
|||||||
activeUserPanel.value = panel;
|
activeUserPanel.value = panel;
|
||||||
userReviewDetail.value = null;
|
userReviewDetail.value = null;
|
||||||
userReviewMessage.value = '';
|
userReviewMessage.value = '';
|
||||||
|
scrollToTop();
|
||||||
|
|
||||||
if (panel === 'sop') {
|
if (panel === 'sop') {
|
||||||
knowledgeQuery.value = '客服接待基础流程';
|
knowledgeQuery.value = '客服接待基础流程';
|
||||||
@@ -1642,10 +1736,11 @@ const openUserAction = (panel: UserPanelKey) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeTabLabel.value =
|
activeTabLabel.value =
|
||||||
({ tasks: '首页', sop: '知识', cases: '知识', submit: '知识', profile: '我的', history: '我的', recommend: '知识' } as Partial<Record<UserPanelKey, string>>)[panel] || '首页';
|
({ tasks: '训练', sop: '知识', cases: '知识', submit: '知识', profile: '我的', history: '我的', recommend: '知识' } as Partial<Record<UserPanelKey, string>>)[panel] || '首页';
|
||||||
activeUserPanel.value = panel;
|
activeUserPanel.value = panel;
|
||||||
userReviewDetail.value = null;
|
userReviewDetail.value = null;
|
||||||
userReviewMessage.value = '';
|
userReviewMessage.value = '';
|
||||||
|
scrollToTop();
|
||||||
|
|
||||||
if (panel === 'cases') {
|
if (panel === 'cases') {
|
||||||
knowledgeQuery.value = '投诉接待优秀案例';
|
knowledgeQuery.value = '投诉接待优秀案例';
|
||||||
@@ -1722,6 +1817,7 @@ const openSupervisorAction = (panel: SupervisorPanelKey) => {
|
|||||||
({ team: '团队', reviews: '团队', assign: '团队', weakness: '团队', case: '案例', sop: '案例', profile: '我的' } as Partial<Record<SupervisorPanelKey, string>>)[panel] || '首页';
|
({ team: '团队', reviews: '团队', assign: '团队', weakness: '团队', case: '案例', sop: '案例', profile: '我的' } as Partial<Record<SupervisorPanelKey, string>>)[panel] || '首页';
|
||||||
activeSupervisorPanel.value = panel;
|
activeSupervisorPanel.value = panel;
|
||||||
activeUserPanel.value = '';
|
activeUserPanel.value = '';
|
||||||
|
scrollToTop();
|
||||||
if (panel === 'reviews' || panel === 'assign') {
|
if (panel === 'reviews' || panel === 'assign') {
|
||||||
void fetchPracticeData();
|
void fetchPracticeData();
|
||||||
}
|
}
|
||||||
@@ -1760,10 +1856,13 @@ const openSupervisorTool = (item: ToolItem) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openTab = (label: string) => {
|
const openTab = (label: string) => {
|
||||||
|
scrollToTop();
|
||||||
activeTabLabel.value = label;
|
activeTabLabel.value = label;
|
||||||
if (roleKey.value === 'supervisor') {
|
if (roleKey.value === 'supervisor') {
|
||||||
if (label === '首页') {
|
if (label === '首页') {
|
||||||
activeSupervisorPanel.value = '';
|
activeSupervisorPanel.value = '';
|
||||||
|
activeUserPanel.value = '';
|
||||||
|
selectedReview.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (label === '团队') {
|
if (label === '团队') {
|
||||||
@@ -1783,6 +1882,8 @@ const openTab = (label: string) => {
|
|||||||
if (roleKey.value === 'candidate') {
|
if (roleKey.value === 'candidate') {
|
||||||
if (label === '首页') {
|
if (label === '首页') {
|
||||||
activeUserPanel.value = '';
|
activeUserPanel.value = '';
|
||||||
|
userReviewDetail.value = null;
|
||||||
|
userReviewMessage.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (label === '面试') {
|
if (label === '面试') {
|
||||||
@@ -1805,14 +1906,19 @@ const openTab = (label: string) => {
|
|||||||
|
|
||||||
if (label === '首页') {
|
if (label === '首页') {
|
||||||
activeUserPanel.value = '';
|
activeUserPanel.value = '';
|
||||||
|
userReviewDetail.value = null;
|
||||||
|
userReviewMessage.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (label === '训练') {
|
if (label === '训练') {
|
||||||
void startMobilePractice();
|
activeUserPanel.value = 'tasks';
|
||||||
|
userReviewDetail.value = null;
|
||||||
|
userReviewMessage.value = '';
|
||||||
|
void fetchPracticeData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (label === '知识') {
|
if (label === '知识') {
|
||||||
openUserAction('sop');
|
switchKnowledgePanel('sop');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (label === '我的') {
|
if (label === '我的') {
|
||||||
@@ -1823,16 +1929,19 @@ const openTab = (label: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openUserHistory = async (item: PracticeRecord) => {
|
const openUserHistory = async (item: PracticeRecord) => {
|
||||||
|
activeTabLabel.value = '我的';
|
||||||
if (!item.id) {
|
if (!item.id) {
|
||||||
activeUserPanel.value = 'history';
|
activeUserPanel.value = 'history';
|
||||||
userReviewMessage.value = '这条训练记录暂无复盘详情';
|
userReviewMessage.value = '这条训练记录暂无复盘详情';
|
||||||
userReviewDetail.value = null;
|
userReviewDetail.value = null;
|
||||||
|
scrollToTop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeUserPanel.value = 'history';
|
activeUserPanel.value = 'history';
|
||||||
userReviewDetail.value = null;
|
userReviewDetail.value = null;
|
||||||
userReviewMessage.value = '';
|
userReviewMessage.value = '';
|
||||||
|
scrollToTop();
|
||||||
try {
|
try {
|
||||||
userReviewDetail.value = await readApi<PracticeReviewDetail>(
|
userReviewDetail.value = await readApi<PracticeReviewDetail>(
|
||||||
await fetch(`/dev-api/api/aihr/mobile/practice/reviews/${item.id}`, { headers: apiHeaders() })
|
await fetch(`/dev-api/api/aihr/mobile/practice/reviews/${item.id}`, { headers: apiHeaders() })
|
||||||
@@ -2031,6 +2140,7 @@ const submitMobilePractice = async () => {
|
|||||||
const startCandidateInterview = async () => {
|
const startCandidateInterview = async () => {
|
||||||
activeUserPanel.value = 'tasks';
|
activeUserPanel.value = 'tasks';
|
||||||
activeTabLabel.value = '面试';
|
activeTabLabel.value = '面试';
|
||||||
|
scrollToTop();
|
||||||
interviewStatus.value = 'starting';
|
interviewStatus.value = 'starting';
|
||||||
interviewSessionId.value = '';
|
interviewSessionId.value = '';
|
||||||
interviewQuestions.value = [];
|
interviewQuestions.value = [];
|
||||||
@@ -2458,6 +2568,7 @@ const countTodayPracticeCompletions = () => {
|
|||||||
const prefix = todayPrefix();
|
const prefix = todayPrefix();
|
||||||
return practiceHistory.value.filter((item) => item.time.startsWith(prefix)).length;
|
return practiceHistory.value.filter((item) => item.time.startsWith(prefix)).length;
|
||||||
};
|
};
|
||||||
|
const todayPracticeCompleted = computed(() => (roleKey.value === 'user' && practiceDataReady.value ? countTodayPracticeCompletions() : 0));
|
||||||
const userPrimaryTask = computed<PrimaryTask>(() => {
|
const userPrimaryTask = computed<PrimaryTask>(() => {
|
||||||
const base = workerRole.value.primary;
|
const base = workerRole.value.primary;
|
||||||
if (roleKey.value !== 'user' || !practiceDataReady.value) return base;
|
if (roleKey.value !== 'user' || !practiceDataReady.value) return base;
|
||||||
@@ -3686,6 +3797,28 @@ p {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.knowledge-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tabs button {
|
||||||
|
min-height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(213, 225, 233, 0.7);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tabs button.active {
|
||||||
|
color: var(--primary-teal-active);
|
||||||
|
background: var(--primary-teal-light);
|
||||||
|
border-color: rgba(15, 174, 166, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
.knowledge-search input {
|
.knowledge-search input {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
@@ -3707,8 +3840,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-search button,
|
.knowledge-search button,
|
||||||
.panel-primary,
|
.panel-primary {
|
||||||
.panel-actions button {
|
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@@ -3869,16 +4001,8 @@ p {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-actions {
|
.growth-summary {
|
||||||
display: flex;
|
margin-top: 0;
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-actions button {
|
|
||||||
min-width: 76px;
|
|
||||||
color: var(--primary-teal-active);
|
|
||||||
background: var(--primary-teal-light);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sop-overlay {
|
.sop-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user