feat(mobile): restructure worker home into scenario-based task flow

员工端首页从功能陈列改为情境任务流,回应甲方「学的时候怎么用、
工作的时候怎么用、碰到问题怎么用」反馈:五区依次为「今天要学」
(训练卡进度接真数据:practiceHistory 按今日 MM-dd 统计,完成后 CTA
变再练一次,接口失败保持静态兜底)、「练完要复盘」(最近一条评分+
导师改写摘要,点击进复盘详情,含空态引导)、「工作中要用」(查SOP
升级为通栏大按钮卡)、「碰到问题要报」(素材上报)、「我的成长」
(进度+能力画像移至底部)。员工端 ToolGrid 移除,候选人/主管端零
改动,demo-check 14 条标记保留。
This commit is contained in:
2026-07-04 14:33:42 +08:00
parent 0f5aa340a7
commit 3dfb98ff1b
+298 -10
View File
@@ -52,7 +52,7 @@
</p>
</header>
<Card :title="roleKey === 'supervisor' ? '今日处理流程' : '今日使用流程'">
<Card v-if="roleKey !== 'user'" :title="roleKey === 'supervisor' ? '今日处理流程' : '今日使用流程'">
<div class="flow-steps">
<button v-for="item in roleFlowSteps" :key="`${item.phase}-${item.title}`" type="button" :class="{ active: isFlowActive(item) }" @click="openFlowStep(item)">
<em>{{ item.phase }}</em>
@@ -278,19 +278,26 @@
</template>
<template v-else>
<Card title="今日任务" action="查看全部" @action="openUserAction('tasks')">
<template v-if="roleKey === 'user'">
<section class="scenario-section">
<div class="scenario-title">
<h2>今天要学</h2>
<p>跟着任务走,学完自动记录</p>
</div>
<div class="scenario-card-stack">
<div class="task-card">
<div class="task-copy">
<strong>{{ workerRole.primary.badge }}</strong>
<h3>{{ workerRole.primary.title }}</h3>
<p>{{ workerRole.primary.desc }}</p>
<span>进度 {{ workerRole.primary.progressText }}</span>
<ProgressBar :value="workerRole.primary.progress" />
<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="workerRole.primary.icon" size="104" stroke="1.7" />
<Icon :name="userPrimaryTask.icon" size="104" stroke="1.7" />
</div>
<button type="button" @click="handlePrimaryAction">{{ workerRole.primary.cta }}</button>
<button type="button" @click="handlePrimaryAction">{{ userPrimaryTask.cta }}</button>
</div>
<button class="review-card" type="button" @click="openUserAction('sop')">
@@ -301,8 +308,9 @@
</span>
<Icon name="chevron-right" size="26" />
</button>
</div>
<div v-if="roleKey === 'user' && practiceStatus !== 'idle'" class="practice-panel">
<div v-if="practiceStatus !== 'idle'" class="practice-panel">
<div class="practice-panel-head">
<span>三角色对练</span>
<b v-if="practiceResult">{{ practiceResult.totalScore }}分</b>
@@ -340,11 +348,94 @@
</div>
<em v-if="practiceMessage">{{ practiceMessage }}</em>
</div>
</section>
<section class="scenario-section">
<div class="scenario-title">
<h2>练完要复盘</h2>
<p>看评分和导师改写,把话术改到位</p>
</div>
<button v-if="latestPracticeReview" class="review-summary-card" type="button" @click="openUserHistory(latestPracticeReview)">
<IconBubble icon="target" tone="blue" />
<span>
<b>{{ latestPracticeReview.scene }} · {{ latestPracticeReview.score }}分</b>
<small>导师改写摘要:{{ latestPracticeReview.summary }}</small>
</span>
<Icon name="chevron-right" size="26" />
</button>
<div v-else class="review-summary-card empty">
<IconBubble icon="target" tone="blue" />
<span>
<b>我的复盘</b>
<small>完成第一次训练后,这里会出现你的评分和导师改写</small>
</span>
</div>
</section>
<section class="scenario-section">
<div class="scenario-title">
<h2>工作中要用</h2>
<p>遇到不确定的现场问题,先查标准做法</p>
</div>
<button class="sop-action-card" type="button" @click="openUserAction('sop')">
<IconBubble icon="book-search" tone="teal" />
<span>
<b>遇到问题,问一句</b>
<small>输入或语音提问,30秒拿到带出处的标准做法</small>
</span>
<Icon name="chevron-right" size="28" />
</button>
</section>
<section class="scenario-section">
<div class="scenario-title">
<h2>碰到问题要报</h2>
<p>现场情况拍下来说出来,沉淀成案例</p>
</div>
<button class="report-card" type="button" @click="openUserAction('submit')">
<IconBubble icon="pen" tone="purple" />
<span>
<b>提交素材 / 案例上报</b>
<small>记录工作场景,上传后自动转写并整理案例</small>
</span>
<Icon name="chevron-right" size="26" />
</button>
</section>
</template>
<template v-else>
<Card title="今日任务" action="查看全部" @action="openUserAction('tasks')">
<div class="task-card">
<div class="task-copy">
<strong>{{ workerRole.primary.badge }}</strong>
<h3>{{ workerRole.primary.title }}</h3>
<p>{{ workerRole.primary.desc }}</p>
<span>进度 {{ workerRole.primary.progressText }}</span>
<ProgressBar :value="workerRole.primary.progress" />
</div>
<div class="headset-art">
<Icon :name="workerRole.primary.icon" size="104" stroke="1.7" />
</div>
<button type="button" @click="handlePrimaryAction">{{ workerRole.primary.cta }}</button>
</div>
<button class="review-card" type="button" @click="openUserAction('sop')">
<IconBubble icon="document" tone="amber" />
<span>
<b>{{ workerRole.review.title }}</b>
<small>{{ workerRole.review.desc }}</small>
</span>
<Icon name="chevron-right" size="26" />
</button>
</Card>
<Card :title="workerRole.toolTitle">
<ToolGrid :items="workerRole.tools" :handler="openUserTool" />
</Card>
</template>
<Card v-if="roleKey !== 'supervisor' && activeUserPanel" :title="userPanelTitle">
<div class="user-panel">
@@ -602,6 +693,12 @@
</div>
</Card>
<section v-if="roleKey === 'user'" class="scenario-section growth-section">
<div class="scenario-title">
<h2>我的成长</h2>
<p>训练、复盘和能力画像集中在这里看</p>
</div>
<Card title="我的进度" :action="workerRole.recommend" compact-action @action="openUserAction('recommend')">
<div class="progress-grid">
<ProgressTile v-for="item in workerRole.metrics" :key="item.title" :item="item" />
@@ -629,6 +726,46 @@
</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>
</Card>
</section>
<Card v-else title="我的进度" :action="workerRole.recommend" compact-action @action="openUserAction('recommend')">
<div class="progress-grid">
<ProgressTile v-for="item in workerRole.metrics" :key="item.title" :item="item" />
</div>
<div v-if="competencyProfile" class="profile-card">
<span>
<small>综合能力</small>
<b>{{ competencyProfile.score }}</b>
</span>
<span>
<small>训练历史</small>
<b>{{ competencyProfile.completed }}</b>
</span>
<span>
<small>待复盘</small>
<b>{{ competencyProfile.pendingReview }}</b>
</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)">
@@ -1061,6 +1198,7 @@ const userReviewMessage = ref('');
const reviewMessage = ref('');
const markingReviewed = ref(false);
const competencyProfile = ref<CompetencyProfile | null>(null);
const practiceDataReady = ref(false);
const activeUserPanel = ref<UserPanelKey>('');
const activeSupervisorPanel = ref<SupervisorPanelKey>('');
const knowledgeQuery = ref('住宅投诉处理流程');
@@ -2214,6 +2352,7 @@ const fetchMobileHome = async () => {
const fetchPracticeData = async () => {
if (!authToken.value || roleKey.value === 'candidate') {
practiceDataReady.value = false;
practiceHistory.value = [];
practiceReviews.value = [];
selectedReview.value = null;
@@ -2231,16 +2370,19 @@ const fetchPracticeData = async () => {
]);
practiceHistory.value = history || [];
competencyProfile.value = profile || null;
practiceDataReady.value = true;
practiceReviews.value = [];
selectedReview.value = null;
return;
}
practiceDataReady.value = false;
practiceReviews.value =
(await readApi<PracticeRecord[]>(await fetch('/dev-api/api/aihr/mobile/practice/reviews', { headers: apiHeaders() }))) || [];
practiceHistory.value = [];
competencyProfile.value = null;
} catch {
practiceDataReady.value = false;
practiceHistory.value = [];
practiceReviews.value = [];
selectedReview.value = null;
@@ -2307,6 +2449,29 @@ const supervisorTeamTiles = computed<MetricItem[]>(() =>
progress: Number.parseInt(item.value, 10) || 66
}))
);
const todayPracticeTarget = 1;
const todayPrefix = () => {
const date = new Date();
return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
const countTodayPracticeCompletions = () => {
const prefix = todayPrefix();
return practiceHistory.value.filter((item) => item.time.startsWith(prefix)).length;
};
const userPrimaryTask = computed<PrimaryTask>(() => {
const base = workerRole.value.primary;
if (roleKey.value !== 'user' || !practiceDataReady.value) return base;
const completed = countTodayPracticeCompletions();
const shownCompleted = Math.min(completed, todayPracticeTarget);
return {
...base,
progressText: `${shownCompleted}/${todayPracticeTarget}`,
progress: Math.round((shownCompleted / todayPracticeTarget) * 100),
cta: completed >= todayPracticeTarget ? '再练一次' : '开始训练'
};
});
const latestPracticeReview = computed(() => practiceHistory.value[0] || null);
const ringStyle = (value: number) => ({ '--value': value } as Record<string, string | number>);
const iconPaths: Record<IconName, Array<[string, Record<string, string | number>?]>> = {
@@ -3081,6 +3246,129 @@ p {
font-weight: 600;
}
.scenario-section {
margin-top: 18px;
display: grid;
gap: 12px;
}
.scenario-title {
display: grid;
grid-template-columns: 4px minmax(0, 1fr);
column-gap: 10px;
align-items: center;
padding: 0 4px;
}
.scenario-title::before {
content: '';
width: 4px;
height: 34px;
border-radius: 99px;
background: var(--primary-teal);
grid-row: 1 / span 2;
}
.scenario-title h2 {
margin: 0;
color: var(--text-primary);
font-size: 18px;
line-height: 1.2;
font-weight: 800;
}
.scenario-title p {
margin-top: 3px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.35;
font-weight: 650;
}
.scenario-card-stack {
display: grid;
gap: 12px;
}
.review-summary-card,
.sop-action-card,
.report-card {
width: 100%;
border: 1px solid var(--glass-border);
border-radius: 18px;
padding: 16px;
background: #ffffff;
box-shadow: var(--shadow-sm);
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 13px;
text-align: left;
transition: var(--transition-spring);
}
.review-summary-card:active,
.sop-action-card:active,
.report-card:active {
transform: scale(0.98);
}
.review-summary-card span,
.sop-action-card span,
.report-card span {
min-width: 0;
}
.review-summary-card b,
.sop-action-card b,
.report-card b {
display: block;
color: var(--text-primary);
font-size: 16px;
line-height: 1.25;
font-weight: 800;
}
.review-summary-card small,
.sop-action-card small,
.report-card small {
display: block;
margin-top: 5px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.4;
font-weight: 600;
}
.review-summary-card.empty {
grid-template-columns: auto minmax(0, 1fr);
}
.sop-action-card {
min-height: 112px;
border-color: rgba(15, 174, 166, 0.28);
background: linear-gradient(135deg, #ecfffd 0%, #ffffff 72%);
box-shadow: 0 12px 28px rgba(9, 151, 144, 0.1);
}
.sop-action-card .icon-bubble {
width: 54px;
height: 54px;
}
.sop-action-card b {
font-size: 19px;
}
.report-card {
border-color: rgba(144, 131, 245, 0.18);
background: linear-gradient(135deg, #fbf7ff 0%, #ffffff 76%);
}
.growth-section .section-card {
margin-top: 0;
}
.practice-panel {
margin-top: 14px;
padding: 16px;