feat(aihr): complete uni-app role workflows

This commit is contained in:
2026-07-09 10:42:20 +08:00
parent e4070c8b8c
commit 678543e8a5
73 changed files with 15696 additions and 134 deletions
+363
View File
@@ -0,0 +1,363 @@
<template>
<div class="growth-page">
<section class="page-head">
<div>
<h1>成长激励</h1>
<p>展示能力画像、初/中/高级证据包、预警、待训任务和学分规则最小集。</p>
</div>
<div class="head-actions">
<el-input v-model="extPartyId" clearable placeholder="员工手机号或 extPartyId" />
<el-button type="danger" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
</div>
</section>
<section class="metric-grid">
<article class="metric-card">
<span>AI测评分</span>
<strong>{{ profile?.assessmentScore ?? '-' }}</strong>
<small>AI辅助参考,人可否决</small>
</article>
<article class="metric-card">
<span>当前等级</span>
<strong>{{ evidence?.currentLevel || profile?.aiLevel || '-' }}</strong>
<small>初/中/高级生活顾问</small>
</article>
<article class="metric-card">
<span>训练时长</span>
<strong>{{ profile?.trainingMinutes ?? 0 }} 分钟</strong>
<small>来自移动端训练记录</small>
</article>
<article class="metric-card">
<span>贡献度</span>
<strong>{{ profile?.contribution ?? 0 }}</strong>
<small>复盘学分 + 训练完成</small>
</article>
</section>
<section class="main-grid">
<article class="panel">
<div class="panel-title">
<h2>能力维度</h2>
</div>
<div class="dimension-list">
<div v-for="item in profile?.dimensions || []" :key="item.label" class="dimension-item">
<div>
<strong>{{ item.label }}</strong>
<span>{{ item.note }}</span>
</div>
<el-progress :percentage="Number(item.value) || 0" :stroke-width="10" color="#e60012" />
</div>
</div>
</article>
<article class="panel">
<div class="panel-title">
<h2>等级路径</h2>
</div>
<div class="stage-list">
<div v-for="stage in profile?.growthPath || []" :key="stage.level" class="stage-item">
<div class="stage-head">
<strong>{{ stage.level }}</strong>
<el-tag :type="stage.status === '已达成' ? 'success' : 'info'">{{ stage.status }}</el-tag>
</div>
<el-progress :percentage="stage.progress" :stroke-width="9" />
<small>{{ stage.evidence }}</small>
</div>
</div>
</article>
</section>
<section class="main-grid">
<article class="panel">
<div class="panel-title">
<h2>晋升证据包</h2>
</div>
<ul class="evidence-list">
<li v-for="item in evidence?.evidenceItems || []" :key="item">{{ item }}</li>
</ul>
</article>
<article class="panel">
<div class="panel-title">
<h2>预警</h2>
</div>
<div class="alert-list">
<div v-for="item in alerts" :key="item.title" class="alert-item" :class="`severity-${item.severity}`">
<strong>{{ item.title }}</strong>
<span>{{ item.desc }}</span>
</div>
</div>
</article>
</section>
<section class="main-grid">
<article class="panel">
<div class="panel-title">
<h2>待训任务</h2>
<el-button type="danger" plain @click="createManualAssignment">派发专项</el-button>
</div>
<el-table :data="assignments" size="small">
<el-table-column prop="traineeName" label="学员" width="110" />
<el-table-column prop="scenarioName" label="训练" min-width="160" />
<el-table-column prop="source" label="来源" width="92" />
<el-table-column prop="status" label="状态" width="92" />
<el-table-column prop="reason" label="原因" min-width="180" show-overflow-tooltip />
</el-table>
</article>
<article class="panel">
<div class="panel-title">
<h2>学分规则最小集</h2>
<el-switch v-model="leaderboardEnabled" active-text="排行榜" inactive-text="默认关闭" />
</div>
<el-table :data="rules" size="small">
<el-table-column prop="source" label="来源" width="130" />
<el-table-column prop="points" label="分值" width="90" />
<el-table-column prop="guard" label="防刷分口径" min-width="220" />
</el-table>
</article>
</section>
</div>
</template>
<script setup name="CompetencyGrowth" lang="ts">
import { Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { onMounted, ref } from 'vue';
import { listPracticeScenarios } from '@/api/aihr/practice';
import {
createPracticeAssignment,
getCompetencyProfile,
getPromotionEvidence,
listPracticeAlerts,
listPracticeAssignments,
type CompetencyProfile,
type PracticeAlert,
type PracticeAssignment,
type PromotionEvidence
} from '@/api/aihr/mobile-admin';
const extPartyId = 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 rules = [
{ source: '训练完成', points: '+5', guard: '只认完成记录,不按点击或打开计分' },
{ source: '主管复盘采纳', points: '+0~100', guard: '由主管复盘时人工确认' },
{ source: '案例被采纳', points: '+20', guard: '必须审核入库并署名' },
{ source: '课程考试通过', points: '+10', guard: '阶段二课程考试上线后启用' }
];
const loadAll = async () => {
loading.value = true;
const party = extPartyId.value.trim();
try {
const [profileResult, evidenceResult, alertsResult, assignmentsResult] = await Promise.all([
getCompetencyProfile(party),
getPromotionEvidence(party),
listPracticeAlerts(),
listPracticeAssignments(party)
]);
profile.value = profileResult.data;
evidence.value = evidenceResult.data;
alerts.value = alertsResult.data || [];
assignments.value = assignmentsResult.data || [];
} catch (error) {
ElMessage.error('成长数据加载失败');
} finally {
loading.value = false;
}
};
const createManualAssignment = async () => {
const party = extPartyId.value.trim() || evidence.value?.extPartyId || '';
if (!party) {
ElMessage.warning('请先输入员工手机号或 extPartyId');
return;
}
try {
const scenarios = (await listPracticeScenarios()).data || [];
const scenario = scenarios.find((item) => item.enabled) || scenarios[0];
await createPracticeAssignment({
extPartyId: party,
traineeName: party,
scenarioId: scenario?.id || 'fee-parking',
scenarioName: scenario?.name,
source: 'manual',
reason: 'HR在成长激励页派发专项训练'
});
ElMessage.success('专项训练已派发');
await loadAll();
} catch (error) {
ElMessage.error('专项训练派发失败');
}
};
onMounted(loadAll);
</script>
<style scoped lang="scss">
.growth-page {
min-height: 100%;
padding: 24px;
background: #f5f7fb;
color: #1f2937;
}
.page-head,
.panel,
.metric-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.page-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 20px 22px;
h1 {
margin: 0 0 8px;
font-size: 24px;
}
p {
margin: 0;
color: #667085;
}
}
.head-actions {
display: grid;
grid-template-columns: 260px auto;
gap: 10px;
}
.metric-grid,
.main-grid {
display: grid;
gap: 16px;
margin-top: 16px;
}
.metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.main-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.metric-card,
.panel {
padding: 18px;
}
.metric-card {
span,
small {
display: block;
color: #667085;
}
strong {
display: block;
margin: 8px 0;
font-size: 24px;
}
}
.panel-title,
.stage-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.panel-title {
margin-bottom: 14px;
h2 {
margin: 0;
font-size: 17px;
}
}
.dimension-list,
.stage-list,
.alert-list {
display: grid;
gap: 14px;
}
.dimension-item strong,
.stage-item strong {
display: block;
margin-bottom: 4px;
}
.dimension-item span,
.stage-item small {
display: block;
margin-bottom: 8px;
color: #667085;
}
.evidence-list {
margin: 0;
padding-left: 18px;
li {
margin-bottom: 10px;
color: #475467;
}
}
.alert-item {
border-radius: 8px;
padding: 12px;
background: #f8fafc;
strong,
span {
display: block;
}
span {
margin-top: 4px;
color: #667085;
}
}
.severity-high {
background: #fff1f3;
}
.severity-medium {
background: #fff7ed;
}
@media (max-width: 1080px) {
.page-head,
.main-grid,
.metric-grid {
grid-template-columns: 1fr;
}
.page-head {
display: block;
}
.head-actions {
margin-top: 14px;
}
}
</style>