fix(mobile): align supervisor profile workspace

This commit is contained in:
2026-07-17 04:42:49 +08:00
parent 5d82ff7b21
commit b3dd2c2cf1
2 changed files with 234 additions and 38 deletions
+221 -38
View File
@@ -2,30 +2,69 @@
<view class="page supervisor-profile-page">
<view class="page-head">
<view>
<text class="page-title">我的主管工作台</text>
<text class="page-subtitle">账号与主管身份</text>
<text class="page-title">我的工作台</text>
<text class="page-subtitle">主管角色、项目范围与业务入口</text>
</view>
</view>
<view v-if="message" class="status-message">{{ message }}</view>
<view class="panel profile-hero">
<view class="profile-avatar">管</view>
<view class="profile-copy">
<text class="profile-role">项目主管</text>
<text class="muted profile-account">{{ displayPhone || '未登录' }} · 住宅项目</text>
<text class="profile-scope">团队画像、复盘、专项派发</text>
</view>
</view>
<view class="profile-metrics" aria-label="主管工作概览">
<view>
<text class="profile-metric-value">{{ loading ? '--' : teamCount }}</text>
<text class="profile-metric-label">团队成员</text>
</view>
<view>
<text class="profile-metric-value">{{ loading ? '--' : pendingReviewCount }}</text>
<text class="profile-metric-label">待复盘</text>
</view>
<view>
<text class="profile-metric-value">{{ loading ? '--' : alertCount }}</text>
<text class="profile-metric-label">风险预警</text>
</view>
</view>
<view class="panel action-card">
<text class="panel-title">主管业务入口</text>
<button class="action-row" @click="openPage('/pages/supervisor/team/index')">
<view>
<text class="action-title">团队画像</text>
<text class="muted action-desc">搜索员工并查看能力详情</text>
</view>
<text class="action-arrow">›</text>
</button>
<button class="action-row" @click="openPage('/pages/supervisor/reviews/index')">
<view>
<text class="action-title">待复盘</text>
<text class="muted action-desc">处理低分与待办记录</text>
</view>
<text class="action-arrow">›</text>
</button>
<button class="action-row" @click="openPage('/pages/supervisor/assign/index')">
<view>
<text class="action-title">指派专项</text>
<text class="muted action-desc">创建任务并跟踪进度</text>
</view>
<text class="action-arrow">›</text>
</button>
</view>
<view class="panel account-card">
<text class="panel-title">当前账号</text>
<text class="account-phone">{{ displayPhone || '未登录' }}</text>
<text class="muted account-note">主管端团队数据仅对当前项目范围可见。</text>
<view>
<text class="panel-title">角色与账号</text>
<text class="muted account-note">登录手机号 {{ displayPhone || '未登录' }}</text>
</view>
<view class="panel identity-card">
<text class="panel-title">主管身份</text>
<view class="identity-row">
<text class="identity-label">角色</text>
<text class="identity-value">项目主管</text>
<button class="text-button logout-button" @click="logout">切换账号</button>
</view>
<view class="identity-row">
<text class="identity-label">可操作范围</text>
<text class="identity-value">团队画像、复盘、专项派发</text>
</view>
</view>
<button class="secondary-button logout-button" @click="logout">退出登录</button>
<SupervisorTabBar active="profile" />
</view>
</template>
@@ -36,74 +75,218 @@ import { onShow } from '@dcloudio/uni-app';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
import { clearAuth, getAuth, isLoggedIn, maskPhone, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { getPracticeAlerts, getPracticeReviews, getPracticeTeamSnapshot } from '@/services/practice';
import { actionablePracticeAlerts, buildTeamProfiles, isSupervisorAccessDenied } from '@/services/supervisor-view';
const currentPath = '/pages/supervisor/profile/index';
const phone = ref('');
const teamCount = ref(0);
const pendingReviewCount = ref(0);
const alertCount = ref(0);
const loading = ref(false);
const message = ref('');
const displayPhone = computed(() => maskPhone(phone.value));
const refreshAuth = () => {
phone.value = getAuth().phone;
};
const clearMetrics = () => {
teamCount.value = 0;
pendingReviewCount.value = 0;
alertCount.value = 0;
};
const loadMetrics = async () => {
if (loading.value) return;
loading.value = true;
message.value = '';
try {
const [reviews, alerts, snapshot] = await Promise.all([
getPracticeReviews(),
getPracticeAlerts(),
getPracticeTeamSnapshot()
]);
teamCount.value = buildTeamProfiles(snapshot.records, snapshot.assignments, snapshot.members).length;
pendingReviewCount.value = reviews.length;
alertCount.value = actionablePracticeAlerts(alerts).length;
} catch (error) {
clearMetrics();
message.value = isSupervisorAccessDenied(error)
? '当前账号无主管权限'
: error instanceof Error ? error.message : '主管数据加载失败';
} finally {
loading.value = false;
}
};
const openPage = (url: string) => {
uni.navigateTo({ url });
};
const logout = () => {
clearAuth();
rememberLoginRedirect('/pages/supervisor/profile/index');
rememberLoginRedirect(currentPath);
uni.redirectTo({ url: '/pages/auth/login/index' });
};
onShow(() => {
resetPageScroll();
if (!isLoggedIn()) {
rememberLoginRedirect('/pages/supervisor/profile/index');
rememberLoginRedirect(currentPath);
uni.redirectTo({ url: '/pages/auth/login/index' });
return;
}
refreshAuth();
void loadMetrics();
});
</script>
<style scoped>
.supervisor-profile-page {
padding-bottom: 112px;
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
}
.account-card,
.identity-card {
.profile-hero {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
gap: 14px;
border-color: #ffd4d7;
}
.account-phone {
.profile-avatar {
display: flex;
width: 54px;
height: 54px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--employee-primary-soft);
color: var(--employee-primary);
font-size: 24px;
font-size: 22px;
font-weight: 900;
}
.profile-copy,
.profile-role,
.profile-account,
.profile-scope,
.profile-metric-value,
.profile-metric-label,
.action-title,
.action-desc,
.account-note {
font-size: 13px;
line-height: 1.5;
display: block;
}
.identity-row {
.profile-copy {
min-width: 0;
flex: 1;
}
.profile-role {
color: var(--employee-text);
font-size: 20px;
font-weight: 900;
}
.profile-account {
margin-top: 5px;
font-size: 13px;
}
.profile-scope {
margin-top: 7px;
color: var(--employee-primary);
font-size: 12px;
}
.profile-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
overflow: hidden;
border: 1px solid var(--employee-line);
border-radius: 18px;
background: #ffffff;
box-shadow: var(--employee-card-shadow);
}
.profile-metrics > view {
padding: 18px 10px;
text-align: center;
}
.profile-metrics > view + view {
border-left: 1px solid var(--employee-line);
}
.profile-metric-value {
color: var(--employee-primary);
font-size: 24px;
font-weight: 900;
line-height: 1;
}
.profile-metric-label {
margin-top: 7px;
color: var(--employee-muted);
font-size: 12px;
}
.action-card {
padding-bottom: 8px;
}
.action-row {
display: flex;
width: 100%;
min-height: 64px;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 10px;
border-top: 1px solid var(--employee-line);
margin: 0;
padding: 12px 0;
border: 0;
border-bottom: 1px solid var(--employee-line);
border-radius: 0;
background: transparent;
text-align: left;
}
.identity-label {
color: var(--employee-muted);
.action-row:last-child {
border-bottom: 0;
}
.identity-value {
.action-title {
color: var(--employee-text);
font-weight: 700;
text-align: right;
font-weight: 800;
}
.action-desc {
margin-top: 4px;
font-size: 12px;
}
.action-arrow {
color: var(--employee-primary);
font-size: 24px;
}
.account-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.account-note {
margin-top: 6px;
font-size: 13px;
}
.logout-button {
width: 100%;
margin-top: 16px;
flex: 0 0 auto;
}
</style>
+13
View File
@@ -289,6 +289,19 @@ test('员工和主管个人页只展示脱敏手机号', async () => {
assert.doesNotMatch(supervisorProfile, /\{\{ phone \|\|/);
});
test('主管个人工作台展示真实指标和三项业务入口', async () => {
const source = await readFile(new URL('../src/pages/supervisor/profile/index.vue', import.meta.url), 'utf8');
assert.match(source, /\{\{ loading \? '--' : teamCount \}\}/);
assert.match(source, /\{\{ loading \? '--' : pendingReviewCount \}\}/);
assert.match(source, /\{\{ loading \? '--' : alertCount \}\}/);
assert.match(source, /主管业务入口/);
assert.match(source, /\/pages\/supervisor\/team\/index/);
assert.match(source, /\/pages\/supervisor\/reviews\/index/);
assert.match(source, /\/pages\/supervisor\/assign\/index/);
assert.match(source, /getPracticeTeamSnapshot/);
});
test('组织快照姓名不可读时用稳定身份标识展示', () => {
const profiles = buildTeamProfiles([], [], [
{ extPartyId: '13900001111', name: '???????' },