feat(aihr): filter supervisor reviews by org scope
This commit is contained in:
+4
-2
@@ -53,8 +53,10 @@ public class AihrMobileController {
|
||||
}
|
||||
|
||||
@GetMapping("/practice/reviews")
|
||||
public R<List<RecordResponse>> practiceReviews() {
|
||||
return R.ok(mobileSeedService.practiceReviews(currentAppUsername()));
|
||||
public R<List<RecordResponse>> practiceReviews(@RequestParam(required = false) String projectCode,
|
||||
@RequestParam(required = false) String extPartyId,
|
||||
@RequestParam(required = false) Integer limit) {
|
||||
return R.ok(mobileSeedService.practiceReviews(currentAppUsername(), projectCode, extPartyId, limit));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/reviews/{id}")
|
||||
|
||||
+4
@@ -57,6 +57,10 @@ public class AihrMobileSeedService {
|
||||
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, 5);
|
||||
}
|
||||
|
||||
public List<RecordResponse> practiceReviews(String supervisorExtPartyId, String projectCode, String extPartyId, Integer limit) {
|
||||
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, projectCode, extPartyId, limit == null ? 20 : limit);
|
||||
}
|
||||
|
||||
public ReviewDetailResponse practiceReview(Long id) {
|
||||
return practiceSeedService.reviewDetail(id);
|
||||
}
|
||||
|
||||
+34
-16
@@ -478,37 +478,55 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
|
||||
public List<RecordResponse> pendingReviewRecords(int limit) {
|
||||
ensurePracticeTable();
|
||||
return jdbcTemplate.query("""
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
|
||||
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
||||
LIMIT ?
|
||||
""", this::mapRecord, TENANT_ID, normalizeLimit(limit));
|
||||
return pendingReviewRecords("", "", "", limit);
|
||||
}
|
||||
|
||||
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, int limit) {
|
||||
return pendingReviewRecords(supervisorExtPartyId, "", "", limit);
|
||||
}
|
||||
|
||||
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, String projectCode, String extPartyId, int limit) {
|
||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||
if (!scope.scoped()) {
|
||||
return pendingReviewRecords(limit);
|
||||
if (scope.scoped() && scope.extPartyIds().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (scope.extPartyIds().isEmpty()) {
|
||||
String project = isBlank(projectCode) ? "" : projectCode.trim();
|
||||
if (!project.isEmpty() && !orgSnapshotTableExists()) {
|
||||
return List.of();
|
||||
}
|
||||
ensurePracticeTable();
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(TENANT_ID);
|
||||
args.addAll(scope.extPartyIds());
|
||||
StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'");
|
||||
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
if (!party.isEmpty()) {
|
||||
where.append(" AND ext_party_id = ?");
|
||||
args.add(party);
|
||||
}
|
||||
if (!project.isEmpty()) {
|
||||
where.append("""
|
||||
AND ext_party_id IN (
|
||||
SELECT ext_party_id
|
||||
FROM aihr_org_snapshot
|
||||
WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active'
|
||||
)
|
||||
""");
|
||||
args.add(TENANT_ID);
|
||||
args.add(project);
|
||||
}
|
||||
if (scope.scoped()) {
|
||||
where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")");
|
||||
args.addAll(scope.extPartyIds());
|
||||
}
|
||||
args.add(normalizeLimit(limit));
|
||||
return jdbcTemplate.query("""
|
||||
String sql = """
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
|
||||
AND ext_party_id IN (%s)
|
||||
""" + where + """
|
||||
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
||||
LIMIT ?
|
||||
""".formatted(inClause(scope.extPartyIds().size())), this::mapRecord, args.toArray());
|
||||
""";
|
||||
return jdbcTemplate.query(sql, this::mapRecord, args.toArray());
|
||||
}
|
||||
|
||||
public String exportPilotCsv() {
|
||||
|
||||
+97
-5
@@ -3,6 +3,7 @@ package org.dromara.aihr.service;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
@@ -11,17 +12,20 @@ import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Tag("dev")
|
||||
public class AihrPracticeSeedServiceTest {
|
||||
|
||||
@Test
|
||||
@@ -64,13 +68,101 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertEquals("只能派发给当前项目范围内员工", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingReviewRecordsAppliesProjectAndAssigneeFilters() {
|
||||
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("主管", List.of("SUP-1", "EMP-1"));
|
||||
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||
|
||||
service.pendingReviewRecords("SUP-1", "P1", "EMP-1", 50);
|
||||
|
||||
assertTrue(jdbcTemplate.reviewSql.contains("status = '待复盘'"));
|
||||
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id = ?"));
|
||||
assertTrue(jdbcTemplate.reviewSql.contains("project_code = ?"));
|
||||
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?,?)"));
|
||||
assertEquals(List.of("000000", "EMP-1", "000000", "P1", "SUP-1", "EMP-1", 20), jdbcTemplate.reviewArgs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingReviewRecordsLimitsNonSupervisorToSelf() {
|
||||
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("员工", List.of("SUP-1", "EMP-1"));
|
||||
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||
|
||||
service.pendingReviewRecords("SUP-1", "", "", 0);
|
||||
|
||||
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?)"));
|
||||
assertEquals(List.of("000000", "SUP-1", 5), jdbcTemplate.reviewArgs);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static <T> List<T> mapOne(InvocationOnMock invocation, Map<String, String> values) throws SQLException {
|
||||
RowMapper mapper = invocation.getArgument(1);
|
||||
ResultSet resultSet = mock(ResultSet.class);
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
when(resultSet.getString(entry.getKey())).thenReturn(entry.getValue());
|
||||
}
|
||||
return List.of((T) mapper.mapRow(resultSet, 0));
|
||||
return mapRows(mapper, List.of(values));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static <T> List<T> mapRows(RowMapper mapper, List<Map<String, String>> rows) throws SQLException {
|
||||
List<T> mapped = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (Map<String, String> values : rows) {
|
||||
ResultSet resultSet = mock(ResultSet.class);
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
when(resultSet.getString(entry.getKey())).thenReturn(entry.getValue());
|
||||
}
|
||||
mapped.add((T) mapper.mapRow(resultSet, index++));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static final class PracticeReviewJdbcTemplate extends JdbcTemplate {
|
||||
private final String ownerLevel;
|
||||
private final List<String> scopeIds;
|
||||
private String reviewSql = "";
|
||||
private List<Object> reviewArgs = List.of();
|
||||
|
||||
private PracticeReviewJdbcTemplate(String ownerLevel, List<String> scopeIds) {
|
||||
this.ownerLevel = ownerLevel;
|
||||
this.scopeIds = scopeIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T queryForObject(String sql, Class<T> requiredType) {
|
||||
return requiredType.cast(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
|
||||
return requiredType.cast(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String sql) {
|
||||
// Table creation is irrelevant for query assembly tests.
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||
try {
|
||||
if (sql.contains("FROM aihr_practice_session")) {
|
||||
reviewSql = sql;
|
||||
reviewArgs = List.of(args);
|
||||
return List.of();
|
||||
}
|
||||
if (sql.contains("SELECT project_code") && sql.contains("LIMIT 1")) {
|
||||
return mapRows(rowMapper, List.of(Map.of(
|
||||
"project_code", "P1",
|
||||
"position_level", ownerLevel
|
||||
)));
|
||||
}
|
||||
if (sql.contains("SELECT ext_party_id") && sql.contains("ORDER BY position_level")) {
|
||||
return mapRows(rowMapper, scopeIds.stream()
|
||||
.map(id -> Map.of("ext_party_id", id))
|
||||
.toList());
|
||||
}
|
||||
return List.of();
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
| 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 |
|
||||
|---|---|---|---|---|
|
||||
| P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 |
|
||||
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,集中待复盘列表、详情、复盘建议、学分、下次训练派发 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” |
|
||||
| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,展示画像、等级路径、证据包、预警、任务与最小规则表 | HR 能看到初/中/高级证据和学分规则口径 |
|
||||
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,并由后端基于组织快照做项目/人员筛选 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” |
|
||||
| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,并用组织快照选择员工查看画像和派发专项 | HR 能看到初/中/高级证据和学分规则口径 |
|
||||
| P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 |
|
||||
| P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 |
|
||||
| P1 | 开放问题榜 | 阶段一只埋 `knowledge_gap`;当前无 UI | 暂不实现,保留 TODO | 阶段二消费 gap 池、上榜、最佳答案、沉淀入库 |
|
||||
@@ -19,4 +19,5 @@
|
||||
- 不新建数据库表。
|
||||
- 不改登录、密钥、模型配置、支付、部署脚本。
|
||||
- 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。
|
||||
- 组织人员快照在 `组织与权限` 页分页展示;业务页只取下拉候选,真实复盘范围由接口参数落到后端过滤。
|
||||
- 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。
|
||||
|
||||
@@ -154,10 +154,17 @@ export type OrgSnapshotResponse = {
|
||||
rows: OrgPersonRow[];
|
||||
};
|
||||
|
||||
export function listPracticeReviews(): Promise<ApiResult<ReviewRecord[]>> {
|
||||
export type ReviewListParams = {
|
||||
projectCode?: string;
|
||||
extPartyId?: string;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export function listPracticeReviews(params?: ReviewListParams): Promise<ApiResult<ReviewRecord[]>> {
|
||||
return request({
|
||||
url: '/api/aihr/mobile/practice/reviews',
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user