fix(mobile): align supervisor profile workspace
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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: '???????' },
|
||||
|
||||
Reference in New Issue
Block a user