diff --git a/docs/PC管理端功能差异TODO.md b/docs/PC管理端功能差异TODO.md
index 0967000c..75be7868 100644
--- a/docs/PC管理端功能差异TODO.md
+++ b/docs/PC管理端功能差异TODO.md
@@ -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 池、上榜、最佳答案、沉淀入库 |
diff --git a/frontend/src/views/competency/growth.vue b/frontend/src/views/competency/growth.vue
index 678f8a2c..d9babba1 100644
--- a/frontend/src/views/competency/growth.vue
+++ b/frontend/src/views/competency/growth.vue
@@ -107,6 +107,12 @@
待训任务
派发专项
+
+
+
+
+
+
@@ -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(null);
const alerts = ref([]);
const assignments = ref([]);
const orgSnapshot = ref(null);
+const scenarios = ref([]);
+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();
});
@@ -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;
}