feat(aihr): complete phase two pilot readiness
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<view class="page reviews-page">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" aria-label="返回主管首页" title="返回主管首页" @click="goBack">←</button>
|
||||
<view class="subpage-heading">
|
||||
<text class="page-title">待复盘</text>
|
||||
<text class="page-subtitle">{{ reviewCountText }}</text>
|
||||
</view>
|
||||
<button v-if="loggedIn && !accessDenied" class="ghost-button refresh-button" :disabled="loading" @click="loadPage">
|
||||
{{ loading ? '刷新中' : '刷新' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="message && !accessDenied" class="status-message">{{ message }}</view>
|
||||
|
||||
<view v-if="accessDenied" class="panel supervisor-access-panel">
|
||||
<text class="panel-title">无主管权限</text>
|
||||
<text class="muted supervisor-access-copy">当前账号不是主管或项目经理,无法查看团队训练数据。</text>
|
||||
<view class="supervisor-access-actions">
|
||||
<button class="secondary-button" @click="goToEmployee">返回员工端</button>
|
||||
<button class="primary-button" @click="switchAccount">切换账号</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!loggedIn" class="panel login-panel">
|
||||
<view>
|
||||
<text class="panel-title">需要登录</text>
|
||||
<text class="muted block">登录后查看待复盘对练。</text>
|
||||
</view>
|
||||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<view v-else class="panel review-panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">复盘队列</text>
|
||||
<text class="muted">低分优先</text>
|
||||
</view>
|
||||
<view v-if="reviews.length" class="review-list">
|
||||
<button
|
||||
v-for="item in reviews"
|
||||
:key="item.id || item.sessionId || item.time"
|
||||
class="review-row"
|
||||
@click="openReview(item)"
|
||||
>
|
||||
<view class="review-copy">
|
||||
<text class="review-title">{{ item.trainee }} · {{ item.scene }}</text>
|
||||
<text class="review-desc">{{ item.time }} · {{ item.summary }}</text>
|
||||
<text class="review-status">{{ item.status }}</text>
|
||||
</view>
|
||||
<view class="review-score">
|
||||
<text>{{ item.score }}</text>
|
||||
<text>查看</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view v-else-if="!loading" class="empty">暂无待复盘记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import type { PracticeAlert, PracticeRecord } from '@/types/api';
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { getPracticeAlerts, getPracticeReviews } from '@/services/practice';
|
||||
import { buildSupervisorMetrics, isSupervisorAccessDenied } from '@/services/supervisor-view';
|
||||
|
||||
const currentPath = '/pages/supervisor/reviews/index';
|
||||
const reviews = ref<PracticeRecord[]>([]);
|
||||
const alerts = ref<PracticeAlert[]>([]);
|
||||
const loggedIn = ref(false);
|
||||
const accessDenied = ref(false);
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
|
||||
const pendingReviewTotal = computed(() =>
|
||||
buildSupervisorMetrics(reviews.value, [], alerts.value)
|
||||
.find((item) => item.key === 'pendingReviews')?.value || 0);
|
||||
const reviewCountText = computed(() =>
|
||||
pendingReviewTotal.value > reviews.value.length
|
||||
? `${reviews.value.length}/${pendingReviewTotal.value} 条`
|
||||
: `${reviews.value.length} 条`);
|
||||
|
||||
const clearData = () => {
|
||||
reviews.value = [];
|
||||
alerts.value = [];
|
||||
};
|
||||
|
||||
const refreshAuthState = () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
};
|
||||
|
||||
const handleRequestError = (error: unknown, fallback: string) => {
|
||||
if (isSupervisorAccessDenied(error)) {
|
||||
clearData();
|
||||
accessDenied.value = true;
|
||||
message.value = '';
|
||||
return;
|
||||
}
|
||||
refreshAuthState();
|
||||
if (!loggedIn.value) {
|
||||
accessDenied.value = false;
|
||||
clearData();
|
||||
message.value = '登录已过期,请重新登录';
|
||||
return;
|
||||
}
|
||||
message.value = error instanceof Error ? error.message : fallback;
|
||||
};
|
||||
|
||||
const loadPage = async () => {
|
||||
refreshAuthState();
|
||||
message.value = '';
|
||||
if (!loggedIn.value) {
|
||||
accessDenied.value = false;
|
||||
clearData();
|
||||
return;
|
||||
}
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const [nextReviews, nextAlerts] = await Promise.all([
|
||||
getPracticeReviews(),
|
||||
getPracticeAlerts()
|
||||
]);
|
||||
accessDenied.value = false;
|
||||
reviews.value = nextReviews;
|
||||
alerts.value = nextAlerts;
|
||||
} catch (error) {
|
||||
handleRequestError(error, '待复盘列表加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openReview = (item: PracticeRecord) => {
|
||||
if (!item.id) {
|
||||
message.value = '该记录缺少复盘ID';
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/supervisor/review/index?id=${item.id}` });
|
||||
};
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect(currentPath);
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
const goToEmployee = () => {
|
||||
uni.switchTab({ url: '/pages/user/today/index' });
|
||||
};
|
||||
|
||||
const switchAccount = () => {
|
||||
clearAuth();
|
||||
rememberLoginRedirect(currentPath);
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (getCurrentPages().length <= 1) {
|
||||
uni.redirectTo({ url: '/pages/supervisor/index/index' });
|
||||
return;
|
||||
}
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => uni.redirectTo({ url: '/pages/supervisor/index/index' })
|
||||
});
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
void loadPage();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reviews-page {
|
||||
padding-bottom: 42px;
|
||||
background: #f4f7fb;
|
||||
}
|
||||
|
||||
.subpage-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-panel,
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
min-width: 78px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.review-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.review-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 88px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 13px 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.review-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.review-copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.review-title,
|
||||
.review-desc,
|
||||
.review-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.review-title {
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.review-desc {
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-status {
|
||||
margin-top: 6px;
|
||||
color: #c2410c;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.review-score {
|
||||
display: flex;
|
||||
min-width: 52px;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.review-score text:first-child {
|
||||
color: #dc2626;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user