feat(aihr): filter supervisor reviews by org scope

This commit is contained in:
2026-07-09 12:43:16 +08:00
parent 45d06f56cd
commit 9bd567a72f
8 changed files with 281 additions and 40 deletions
+50 -5
View File
@@ -6,7 +6,17 @@
<p>展示能力画像、初/中/高级证据包、预警、待训任务和学分规则最小集。</p>
</div>
<div class="head-actions">
<el-input v-model="extPartyId" clearable placeholder="员工手机号或 extPartyId" />
<el-select v-model="selectedProjectCode" clearable filterable placeholder="项目范围">
<el-option v-for="item in orgSnapshot?.projects || []" :key="item.projectCode" :label="item.projectName" :value="item.projectCode" />
</el-select>
<el-select v-model="extPartyId" clearable filterable allow-create default-first-option placeholder="组织人员或 extPartyId">
<el-option
v-for="person in orgPeople"
:key="person.extPartyId"
:label="`${person.personName} / ${person.positionName}`"
:value="person.extPartyId"
/>
</el-select>
<el-button type="danger" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
</div>
</section>
@@ -123,27 +133,31 @@
<script setup name="CompetencyGrowth" lang="ts">
import { Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { listPracticeScenarios } from '@/api/aihr/practice';
import {
createPracticeAssignment,
getCompetencyProfile,
getOrgSnapshot,
getPromotionEvidence,
listPracticeAlerts,
listPracticeAssignments,
type CompetencyProfile,
type OrgSnapshotResponse,
type PracticeAlert,
type PracticeAssignment,
type PromotionEvidence
} from '@/api/aihr/mobile-admin';
const extPartyId = ref('');
const selectedProjectCode = ref('');
const loading = ref(false);
const leaderboardEnabled = ref(false);
const profile = ref<CompetencyProfile | null>(null);
const evidence = ref<PromotionEvidence | null>(null);
const alerts = ref<PracticeAlert[]>([]);
const assignments = ref<PracticeAssignment[]>([]);
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
const rules = [
{ source: '训练完成', points: '+5', guard: '只认完成记录,不按点击或打开计分' },
@@ -152,6 +166,23 @@ const rules = [
{ source: '课程考试通过', points: '+10', guard: '阶段二课程考试上线后启用' }
];
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
const selectedPerson = computed(() => orgPeople.value.find((item) => item.extPartyId === extPartyId.value));
const loadOrgSnapshot = async () => {
try {
const result = await getOrgSnapshot({
status: 'active',
projectCode: selectedProjectCode.value,
pageNum: 1,
pageSize: 500
});
orgSnapshot.value = result.data;
} catch (error) {
orgSnapshot.value = null;
}
};
const loadAll = async () => {
loading.value = true;
const party = extPartyId.value.trim();
@@ -184,7 +215,7 @@ const createManualAssignment = async () => {
const scenario = scenarios.find((item) => item.enabled) || scenarios[0];
await createPracticeAssignment({
extPartyId: party,
traineeName: party,
traineeName: selectedPerson.value?.personName || party,
scenarioId: scenario?.id || 'fee-parking',
scenarioName: scenario?.name,
source: 'manual',
@@ -197,7 +228,19 @@ const createManualAssignment = async () => {
}
};
onMounted(loadAll);
watch(selectedProjectCode, async () => {
extPartyId.value = '';
await loadOrgSnapshot();
});
watch(extPartyId, () => {
loadAll();
});
onMounted(() => {
loadOrgSnapshot();
loadAll();
});
</script>
<style scoped lang="scss">
@@ -236,7 +279,7 @@ onMounted(loadAll);
.head-actions {
display: grid;
grid-template-columns: 260px auto;
grid-template-columns: 220px 280px auto;
gap: 10px;
}
@@ -357,6 +400,8 @@ onMounted(loadAll);
}
.head-actions {
display: grid;
grid-template-columns: 1fr;
margin-top: 14px;
}
}
+80 -8
View File
@@ -14,9 +14,22 @@
<article class="panel list-panel">
<div class="panel-title">
<h2>待复盘记录</h2>
<el-tag type="danger">{{ reviews.length }} 条</el-tag>
<el-tag type="danger">{{ visibleReviews.length }} 条</el-tag>
</div>
<el-table v-loading="loading" :data="reviews" size="small" highlight-current-row @row-click="selectReview">
<div class="list-filters">
<el-select v-model="selectedProjectCode" clearable filterable placeholder="项目范围">
<el-option v-for="item in orgSnapshot?.projects || []" :key="item.projectCode" :label="item.projectName" :value="item.projectCode" />
</el-select>
<el-select v-model="selectedExtPartyId" clearable filterable placeholder="组织人员">
<el-option
v-for="person in orgPeople"
:key="person.extPartyId"
:label="`${person.personName} / ${person.positionName}`"
:value="person.extPartyId"
/>
</el-select>
</div>
<el-table v-loading="loading" :data="visibleReviews" size="small" highlight-current-row @row-click="selectReview">
<el-table-column prop="trainee" label="学员" width="96" />
<el-table-column prop="scene" label="场景" min-width="150" show-overflow-tooltip />
<el-table-column prop="score" label="得分" width="70" />
@@ -92,15 +105,26 @@
<script setup name="TrainReviews" lang="ts">
import { Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
import { getPracticeReview, listPracticeReviews, markPracticeReviewed, type ReviewDetail, type ReviewRecord } from '@/api/aihr/mobile-admin';
import {
getOrgSnapshot,
getPracticeReview,
listPracticeReviews,
markPracticeReviewed,
type OrgSnapshotResponse,
type ReviewDetail,
type ReviewRecord
} from '@/api/aihr/mobile-admin';
const loading = ref(false);
const saving = ref(false);
const reviews = ref<ReviewRecord[]>([]);
const detail = ref<ReviewDetail | null>(null);
const scenarios = ref<PracticeScenarioResponse[]>([]);
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
const selectedProjectCode = ref('');
const selectedExtPartyId = ref('');
const reviewForm = reactive({
reviewer: '主管',
@@ -110,6 +134,9 @@ const reviewForm = reactive({
nextReason: ''
});
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
const visibleReviews = computed(() => reviews.value);
const loadScenarios = async () => {
try {
const result = await listPracticeScenarios();
@@ -119,14 +146,30 @@ const loadScenarios = async () => {
}
};
const loadOrgSnapshot = async () => {
try {
const result = await getOrgSnapshot({
status: 'active',
projectCode: selectedProjectCode.value,
pageNum: 1,
pageSize: 500
});
orgSnapshot.value = result.data;
} catch (error) {
orgSnapshot.value = null;
}
};
const loadReviews = async () => {
loading.value = true;
try {
const result = await listPracticeReviews();
const result = await listPracticeReviews({
projectCode: selectedProjectCode.value,
extPartyId: selectedExtPartyId.value,
limit: 50
});
reviews.value = result.data || [];
if (!detail.value && reviews.value.length > 0) {
await selectReview(reviews.value[0]);
}
await selectFirstVisibleReview();
} catch (error) {
ElMessage.error('待复盘列表加载失败');
} finally {
@@ -143,6 +186,16 @@ const selectReview = async (row: ReviewRecord) => {
reviewForm.nextReason = '';
};
const selectFirstVisibleReview = async () => {
if (visibleReviews.value.some((item) => item.id === detail.value?.id)) return;
const first = visibleReviews.value[0];
if (!first) {
detail.value = null;
return;
}
await selectReview(first);
};
const submitReview = async () => {
if (!detail.value) return;
saving.value = true;
@@ -164,8 +217,19 @@ const submitReview = async () => {
}
};
watch(selectedProjectCode, async () => {
selectedExtPartyId.value = '';
await loadOrgSnapshot();
await loadReviews();
});
watch(selectedExtPartyId, () => {
loadReviews();
});
onMounted(() => {
loadScenarios();
loadOrgSnapshot();
loadReviews();
});
</script>
@@ -227,6 +291,13 @@ onMounted(() => {
}
}
.list-filters {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 10px;
margin-bottom: 12px;
}
.score-grid,
.form-grid {
display: grid;
@@ -287,6 +358,7 @@ onMounted(() => {
@media (max-width: 1080px) {
.review-layout,
.list-filters,
.score-grid,
.form-grid {
grid-template-columns: 1fr;