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