feat(frontend): make growth assignments selectable

This commit is contained in:
2026-07-10 00:04:39 +08:00
parent 6b8ccd5a35
commit baf27a5de9
2 changed files with 38 additions and 8 deletions
+2 -2
View File
@@ -5,9 +5,9 @@
| 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 |
|---|---|---|---|---|
| P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 |
| P0 | 组织人员 / 岗位 / 项目权限运营页 | 已挂 `组织与权限` 页面,接组织快照查询、项目/岗位/层级/状态筛选、同步预检和质量提示 | 保留组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 |
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 已挂 `复盘校准` 页面,接真实复盘、评分校准、学分和派发接口 | 保留一页式处理:看记录、写建议、校准总分、给学分、派训练 | 主管能从一页完成“看记录 -> 写建议 -> 校准评分 -> 派训练” |
| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,并用组织快照选择员工查看画像和派发专项 | HR 能看到初/中/高级证据和学分规则口径 |
| P0 | 能力画像 / 等级证据包 / 激励规则 | 已挂 `成长激励` 页面,接画像、晋升证据包、预警、待训任务和专项派发接口 | 在成长页选择员工、专项场景和派发原因后派训练;规则先展示最小运营口径 | HR 能看到初/中/高级证据和学分规则口径,并能按场景派发专项 |
| P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 |
| P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 |
| P1 | 开放问题榜 | 阶段一只埋 `knowledge_gap`;当前无 UI | 暂不实现,保留 TODO | 阶段二消费 gap 池、上榜、最佳答案、沉淀入库 |
+36 -6
View File
@@ -107,6 +107,12 @@
<h2>待训任务</h2>
<el-button type="danger" plain @click="createManualAssignment">派发专项</el-button>
</div>
<div class="assignment-form">
<el-select v-model="assignmentScenarioId" filterable placeholder="选择专项场景">
<el-option v-for="scenario in scenarios" :key="scenario.id" :label="scenario.name" :value="scenario.id" />
</el-select>
<el-input v-model="assignmentReason" placeholder="派发原因" />
</div>
<el-table :data="assignments" size="small">
<el-table-column prop="traineeName" label="学员" width="110" />
<el-table-column prop="scenarioName" label="训练" min-width="160" />
@@ -136,7 +142,7 @@ import { Download, Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import FileSaver from 'file-saver';
import { computed, onMounted, ref, watch } from 'vue';
import { listPracticeScenarios } from '@/api/aihr/practice';
import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
import {
createPracticeAssignment,
getCompetencyProfile,
@@ -160,6 +166,9 @@ const evidence = ref<PromotionEvidence | null>(null);
const alerts = ref<PracticeAlert[]>([]);
const assignments = ref<PracticeAssignment[]>([]);
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
const scenarios = ref<PracticeScenarioResponse[]>([]);
const assignmentScenarioId = ref('');
const assignmentReason = ref('HR在成长激励页派发专项训练');
const rules = [
{ source: '训练完成', points: '+5', guard: '只认完成记录,不按点击或打开计分' },
@@ -206,22 +215,34 @@ const loadAll = async () => {
}
};
const loadScenarios = async () => {
try {
scenarios.value = ((await listPracticeScenarios()).data || []).filter((item) => item.enabled);
assignmentScenarioId.value = assignmentScenarioId.value || scenarios.value[0]?.id || '';
} catch (error) {
scenarios.value = [];
}
};
const createManualAssignment = async () => {
const party = extPartyId.value.trim() || evidence.value?.extPartyId || '';
if (!party) {
ElMessage.warning('请先输入员工手机号或 extPartyId');
return;
}
const scenario = scenarios.value.find((item) => item.id === assignmentScenarioId.value);
if (!scenario) {
ElMessage.warning('请先选择专项场景');
return;
}
try {
const scenarios = (await listPracticeScenarios()).data || [];
const scenario = scenarios.find((item) => item.enabled) || scenarios[0];
await createPracticeAssignment({
extPartyId: party,
traineeName: selectedPerson.value?.personName || party,
scenarioId: scenario?.id || 'fee-parking',
scenarioName: scenario?.name,
scenarioId: scenario.id,
scenarioName: scenario.name,
source: 'manual',
reason: 'HR在成长激励页派发专项训练'
reason: assignmentReason.value.trim() || 'HR在成长激励页派发专项训练'
});
ElMessage.success('专项训练已派发');
await loadAll();
@@ -279,6 +300,7 @@ watch(extPartyId, () => {
onMounted(() => {
loadOrgSnapshot();
loadScenarios();
loadAll();
});
</script>
@@ -374,6 +396,13 @@ onMounted(() => {
}
}
.assignment-form {
display: grid;
grid-template-columns: minmax(180px, 0.75fr) minmax(220px, 1fr);
gap: 10px;
margin-bottom: 12px;
}
.dimension-list,
.stage-list,
.alert-list {
@@ -431,6 +460,7 @@ onMounted(() => {
@media (max-width: 1080px) {
.page-head,
.main-grid,
.assignment-form,
.metric-grid {
grid-template-columns: 1fr;
}