feat(mobile): redesign profile pages per two-role design spec

Employee profile moves the growth summary to a compact five-metric
strip with a right-aligned detail pill, masks the phone number, adds
red-filled entry icons, keeps the three key evidence scores always
visible, and switches history rows to icon + inline score with a
view-all link. Supervisor profile masks the account phone as well.
This commit is contained in:
2026-07-20 15:42:13 +08:00
parent 2dfee9f7ee
commit eea1ca1c6f
2 changed files with 194 additions and 115 deletions
@@ -9,7 +9,7 @@
<view class="panel account-card">
<text class="panel-title">当前账号</text>
<text class="account-phone">{{ phone || '未登录' }}</text>
<text class="account-phone">{{ maskedPhone }}</text>
<text class="muted account-note">主管端团队数据仅对当前项目范围可见。</text>
</view>
@@ -37,7 +37,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
import {
@@ -51,6 +51,12 @@ import { resetPageScroll } from '@/services/navigation';
const phone = ref('');
const maskedPhone = computed(() => {
const value = phone.value.trim();
if (!value) return '未登录';
return /^1[3-9]\d{9}$/.test(value) ? `${value.slice(0, 3)}****${value.slice(7)}` : value;
});
const refreshAuth = () => {
phone.value = getAuth().phone;
};
+186 -113
View File
@@ -13,13 +13,13 @@
<uni-icons type="person-filled" size="26" color="#e60012" />
</view>
<view class="identity-info">
<text class="identity-phone">{{ phone || '未登录' }}</text>
<text class="identity-phone">{{ maskedPhone }}</text>
<text v-if="loggedIn" class="identity-position">{{ positionProfile.value }} · {{ positionProfile.heroTitle }}</text>
<text v-if="loggedIn" class="identity-source">岗位来自人力系统组织同步,如需调整请联系 HR</text>
<text v-else class="identity-source">登录后查看训练记录和能力画像</text>
</view>
<button v-if="!loggedIn" class="primary-button identity-action" @click="goLogin">去登录</button>
<text v-else class="identity-logout" role="button" @click="logout">退出</text>
<text v-else class="identity-logout" role="button" @click="logout">退出 ›</text>
</view>
<view v-if="supervisorLearning" class="panel supervisor-mode-panel">
@@ -32,34 +32,34 @@
<view v-if="loggedIn && profile" class="panel profile-summary">
<text class="panel-title">成长摘要</text>
<text class="ai-score-disclaimer">AI训练综合分仅作训练参考,不等于BRD五维绩效总分,人可复核/否决</text>
<view class="score-row">
<view class="score-main">
<text class="score-value">{{ profile.score }}</text>
<text class="score-label">训练综合分</text>
<view class="summary-strip">
<view class="summary-cell">
<text class="summary-value score">{{ profile.score }}</text>
<text class="summary-label">训练综合分</text>
</view>
<view class="metric-grid">
<view class="metric">
<text>{{ profile.completed }}</text>
<text>已训练</text>
</view>
<view class="metric">
<text>{{ profile.pendingReview }}</text>
<text>待复盘</text>
</view>
<view class="metric">
<text>{{ profile.trainingMinutes || 0 }}</text>
<text>分钟</text>
</view>
<view class="metric">
<text>{{ profile.aiLevel || '未评级' }}</text>
<text>训练画像等级(参考)</text>
</view>
<view class="summary-cell">
<text class="summary-value">{{ profile.completed }}</text>
<text class="summary-label">已训练</text>
</view>
<view class="summary-cell">
<text class="summary-value">{{ profile.pendingReview }}</text>
<text class="summary-label">待复盘</text>
</view>
<view class="summary-cell">
<text class="summary-value">{{ profile.trainingMinutes || 0 }}</text>
<text class="summary-label">分钟</text>
</view>
<view class="summary-cell">
<text class="summary-value level">{{ profile.aiLevel || '未评级' }}</text>
<text class="summary-label">等级</text>
</view>
</view>
<button class="fold-toggle" @click="showGrowthDetail = !showGrowthDetail">
{{ showGrowthDetail ? '收起能力详情 ▲' : '展开能力详情(五维评分 · 成长路径)▼' }}
</button>
<text class="ai-score-disclaimer">AI训练综合分仅作训练参考,不等于BRD五维绩效总分,人可复核/否决</text>
<view class="summary-foot">
<button class="growth-toggle" @click="showGrowthDetail = !showGrowthDetail">
{{ showGrowthDetail ? '收起 ›' : '展开能力详情 ›' }}
</button>
</view>
<view v-if="showGrowthDetail && profile.dimensions?.length" class="dimension-list">
<view v-for="item in profile.dimensions" :key="item.label" class="dimension-row">
<view>
@@ -89,9 +89,12 @@
</view>
<button v-if="loggedIn" class="panel points-entry" @click="openPoints">
<view>
<view class="entry-icon">
<uni-icons type="star-filled" size="20" color="#ffffff" />
</view>
<view class="points-entry-copy">
<text class="panel-title">我的积分</text>
<text class="points-entry-desc">查看问题榜奖励积分与学习学分明细</text>
<text class="points-entry-desc">奖励积分与学习学分明细</text>
</view>
<text class="points-entry-arrow">›</text>
</button>
@@ -99,18 +102,18 @@
<view v-if="loggedIn" class="panel feature-entries">
<text class="panel-title">我的助手</text>
<button class="feature-entry" @click="openMemories">
<view class="feature-entry-icon">
<uni-icons type="auth" size="20" color="#e60012" />
<view class="entry-icon">
<uni-icons type="auth-filled" size="20" color="#ffffff" />
</view>
<view class="feature-entry-copy">
<text class="feature-entry-title">我的资料与记忆</text>
<text class="feature-entry-desc">确认待保存事项 · 查看项目跟进</text>
<text class="feature-entry-desc">待确认 · 私人记录 · 项目跟进</text>
</view>
<text class="points-entry-arrow">›</text>
</button>
<button class="feature-entry" @click="openPracticeSummary">
<view class="feature-entry-icon">
<uni-icons type="list" size="20" color="#e60012" />
<view class="entry-icon">
<uni-icons type="list" size="20" color="#ffffff" />
</view>
<view class="feature-entry-copy">
<text class="feature-entry-title">我的训练概况</text>
@@ -124,27 +127,29 @@
<view class="fold-head">
<text class="panel-title">成长证据包(待人工复核)</text>
<button class="fold-toggle inline" @click="showEvidence = !showEvidence">
{{ showEvidence ? '收起 ▲' : '展开 ▼' }}
{{ showEvidence ? '收起 ›' : '展开 ›' }}
</button>
</view>
<view class="evidence-strip">
<view class="evidence-cell">
<text class="evidence-value">{{ evidence.assessmentScore }}</text>
<text class="evidence-label">AI测评分</text>
</view>
<view class="evidence-cell">
<text class="evidence-value">{{ evidence.humanAssessmentScore ?? '待采集' }}</text>
<text class="evidence-label">人工校准分</text>
</view>
<view class="evidence-cell">
<text class="evidence-value">{{ evidence.finalAdoptedScore ?? evidence.assessmentScore }}</text>
<text class="evidence-label">最终采用分</text>
</view>
</view>
<template v-if="showEvidence">
<view class="evidence-grid">
<view class="evidence-metric">
<text>{{ evidence.currentLevel }}</text>
<text>参考等级</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.assessmentScore }}</text>
<text>AI测评分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.humanAssessmentScore ?? '待采集' }}</text>
<text>人工校准分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.finalAdoptedScore ?? evidence.assessmentScore }}</text>
<text>最终采用分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.trainingMinutes }}</text>
<text>训练分钟</text>
@@ -172,17 +177,17 @@
class="history-row history-row-clickable"
@click="openHistory(item.id)"
>
<view>
<text class="history-title">{{ item.scene }}</text>
<text class="history-desc">{{ item.time }} · {{ item.summary }}</text>
<view class="entry-icon small">
<uni-icons type="compose" size="16" color="#ffffff" />
</view>
<view class="history-score">
<text>{{ item.score }}</text>
<text>{{ item.status }}</text>
<view class="history-main">
<text class="history-title">{{ item.scene }} · {{ historyMark(item) }}</text>
<text class="history-desc">{{ item.time }}</text>
</view>
<text class="points-entry-arrow">›</text>
</view>
<button v-if="history.length > 3" class="fold-toggle" @click="showAllHistory = !showAllHistory">
{{ showAllHistory ? '收起 ▲' : `展开全部 ${history.length} 条 ▼` }}
<button v-if="history.length > 3" class="history-more" @click="showAllHistory = !showAllHistory">
{{ showAllHistory ? '收起 ›' : `查看全部 ${history.length} 条 ›` }}
</button>
</view>
<view v-else class="empty">暂无训练记录</view>
@@ -190,11 +195,14 @@
<view class="panel display-panel">
<button class="display-heading" @click="showFontOptions = !showFontOptions">
<view class="entry-icon">
<text class="display-icon-text">Aa</text>
</view>
<view class="display-heading-copy">
<text class="panel-title">显示与辅助</text>
<text class="display-current">字体大小 · {{ currentFontSizeLabel }}</text>
</view>
<text class="display-toggle">{{ showFontOptions ? '收起' : '调整' }}</text>
<text class="display-toggle">{{ showFontOptions ? '收起 ›' : '调整 ›' }}</text>
</button>
<template v-if="showFontOptions">
<view class="font-size-options" role="group" aria-label="字体大小">
@@ -256,6 +264,13 @@ const fontSize = ref<AppFontSize>(getAppFontSize());
const currentFontSizeLabel = computed(() =>
appFontSizeOptions.find((item) => item.value === fontSize.value)?.label || '标准'
);
const maskedPhone = computed(() => {
const value = phone.value.trim();
if (!value) return '未登录';
return /^1[3-9]\d{9}$/.test(value) ? `${value.slice(0, 3)}****${value.slice(7)}` : value;
});
const historyMark = (item: PracticeRecord) =>
item.status === '待复盘' ? '待复盘' : `${item.score}分`;
let profileLoadGeneration = 0;
let profileAccountPhone = '';
@@ -528,6 +543,7 @@ onShow(() => {
.display-heading-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
@@ -582,71 +598,74 @@ onShow(() => {
border-color: var(--employee-line);
}
.score-row {
display: grid;
margin-top: 16px;
grid-template-columns: 104px 1fr;
gap: 14px;
.summary-strip {
display: flex;
margin-top: 14px;
align-items: flex-start;
justify-content: space-between;
}
.ai-score-disclaimer {
display: block;
margin-top: 8px;
margin-top: 10px;
color: var(--employee-muted);
font-size: 12px;
line-height: 1.45;
}
.score-main {
.summary-cell {
display: flex;
min-height: 124px;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 18px;
background: #f5f7fa;
gap: 4px;
}
.score-value {
.summary-value {
color: var(--employee-text);
font-size: 42px;
font-size: 22px;
font-weight: 900;
line-height: 1;
line-height: 1.15;
}
.score-label {
margin-top: 8px;
.summary-value.score {
color: var(--brand);
}
.summary-value.level {
font-size: 17px;
}
.summary-label {
color: var(--employee-muted);
font-size: 13px;
font-size: 11px;
white-space: nowrap;
}
.metric-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.metric {
.summary-foot {
display: flex;
min-height: 58px;
flex-direction: column;
justify-content: center;
padding: 8px 10px;
border: 1px solid var(--employee-line);
border-radius: 14px;
background: #fbfdff;
margin-top: 10px;
justify-content: flex-end;
}
.metric text:first-child {
color: var(--employee-text);
font-size: 18px;
font-weight: 900;
}
.metric text:last-child {
margin-top: 4px;
color: var(--employee-muted);
.growth-toggle {
display: flex;
min-height: 30px;
align-items: center;
margin: 0;
padding: 0 12px;
border: 1px solid var(--brand);
border-radius: 999px;
background: #ffffff;
color: var(--brand);
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
.growth-toggle::after {
border: none;
}
.evidence-panel {
@@ -655,6 +674,35 @@ onShow(() => {
background: #fffaf2;
}
.evidence-strip {
display: flex;
margin-top: 14px;
align-items: flex-start;
justify-content: space-between;
}
.evidence-cell {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: center;
gap: 4px;
}
.evidence-value {
color: #b56b14;
font-size: 20px;
font-weight: 900;
line-height: 1.15;
}
.evidence-label {
color: var(--employee-muted);
font-size: 11px;
white-space: nowrap;
}
.evidence-grid {
display: grid;
margin-top: 14px;
@@ -774,11 +822,16 @@ onShow(() => {
display: flex;
width: calc(100% - 0px);
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--ink);
text-align: left;
}
.points-entry-copy {
flex: 1;
min-width: 0;
}
.points-entry-desc {
display: block;
margin-top: 5px;
@@ -819,9 +872,11 @@ onShow(() => {
}
.dimension-score {
flex-shrink: 0;
color: var(--employee-text);
font-size: 20px;
font-weight: 900;
white-space: nowrap;
}
.dimension-score.pending {
@@ -860,23 +915,29 @@ onShow(() => {
opacity: 0.72;
}
.history-score {
.history-main {
flex: 1;
min-width: 0;
}
.history-more {
display: flex;
min-width: 52px;
flex-direction: column;
align-items: flex-end;
align-items: center;
justify-content: center;
width: 100%;
min-height: 36px;
margin: 6px 0 0;
padding: 0;
border: none;
border-top: 1px dashed #eceff2;
background: transparent;
color: #526176;
font-size: 13px;
line-height: 1.3;
}
.history-score text:first-child {
color: var(--employee-text);
font-size: 20px;
font-weight: 900;
}
.history-score text:last-child {
margin-top: 4px;
color: var(--employee-muted);
font-size: 12px;
.history-more::after {
border: none;
}
.feature-entries {
@@ -906,7 +967,7 @@ onShow(() => {
border-top: 1px solid #f2f3f5;
}
.feature-entry-icon {
.entry-icon {
flex-shrink: 0;
display: flex;
align-items: center;
@@ -914,7 +975,19 @@ onShow(() => {
width: 38px;
height: 38px;
border-radius: 10px;
background: var(--employee-primary-soft);
background: var(--brand);
}
.entry-icon.small {
width: 32px;
height: 32px;
border-radius: 9px;
}
.display-icon-text {
color: #ffffff;
font-size: 15px;
font-weight: 900;
}
.feature-entry-copy {