Преглед на файлове

feat: 增加周期状态就绪检查

wangkangyjy преди 1 месец
родител
ревизия
d94d11575a
променени са 54 файла, в които са добавени 179 реда и са изтрити 16 реда
  1. 6 0
      backend/src/main/java/com/yimeng/okr/controller/AssessmentPeriodController.java
  2. 16 0
      backend/src/main/java/com/yimeng/okr/dto/PeriodReadinessDto.java
  3. 71 0
      backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java
  4. 0 0
      backend/src/main/resources/static/assets/AlignmentView-DKI5FD_z.js
  5. 0 0
      backend/src/main/resources/static/assets/DimensionConfigView-MTWy69Yi.js
  6. 0 0
      backend/src/main/resources/static/assets/FeedbackView-B_WRJxa3.js
  7. 0 0
      backend/src/main/resources/static/assets/HistoryView-DHvaPeyY.js
  8. 0 0
      backend/src/main/resources/static/assets/KpiTemplateView-Cv00xKsI.js
  9. 0 0
      backend/src/main/resources/static/assets/LogView-TQOFYcmz.js
  10. 0 0
      backend/src/main/resources/static/assets/LoginView-Di359Fkb.js
  11. 0 1
      backend/src/main/resources/static/assets/MainLayout-LaLCHCLE.js
  12. 0 0
      backend/src/main/resources/static/assets/MyOkrView-eEkrPM1k.js
  13. 0 0
      backend/src/main/resources/static/assets/MyResultView-BaSnzu4O.js
  14. 0 0
      backend/src/main/resources/static/assets/NotificationView-CeRAXi5F.js
  15. 0 0
      backend/src/main/resources/static/assets/OrgView-DBmJYd-4.js
  16. 0 0
      backend/src/main/resources/static/assets/PeriodBanner-DqtCk_tX.js
  17. 0 0
      backend/src/main/resources/static/assets/PeriodListView-BeneuKGr.css
  18. 0 0
      backend/src/main/resources/static/assets/PeriodListView-C6lJkC6c.js
  19. 0 0
      backend/src/main/resources/static/assets/PeriodListView-Dh3VFwV9.js
  20. 0 0
      backend/src/main/resources/static/assets/PeriodListView-DynDSbMv.css
  21. 0 0
      backend/src/main/resources/static/assets/ScoringEditView-CmLeL416.js
  22. 0 0
      backend/src/main/resources/static/assets/ScoringListView-CqWQ_EvB.js
  23. 0 1
      backend/src/main/resources/static/assets/TeamOkrView-BCrZRD04.js
  24. 1 1
      backend/src/main/resources/static/assets/auth-B53ss9y5.js
  25. 1 1
      backend/src/main/resources/static/assets/index-BPukwgZW.js
  26. 0 0
      backend/src/main/resources/static/assets/index-C1NeD1gi.js
  27. 1 1
      backend/src/main/resources/static/index.html
  28. 63 0
      backend/src/test/java/com/yimeng/okr/service/AssessmentPeriodServiceTest.java
  29. 0 0
      frontend/dist/assets/AlignmentView-DKI5FD_z.js
  30. 0 0
      frontend/dist/assets/DimensionConfigView-MTWy69Yi.js
  31. 0 0
      frontend/dist/assets/FeedbackView-B_WRJxa3.js
  32. 0 0
      frontend/dist/assets/HistoryView-DHvaPeyY.js
  33. 0 0
      frontend/dist/assets/KpiTemplateView-Cv00xKsI.js
  34. 0 0
      frontend/dist/assets/LogView-TQOFYcmz.js
  35. 0 0
      frontend/dist/assets/LoginView-Di359Fkb.js
  36. 0 1
      frontend/dist/assets/MainLayout-LaLCHCLE.js
  37. 0 0
      frontend/dist/assets/MyOkrView-eEkrPM1k.js
  38. 0 0
      frontend/dist/assets/MyResultView-BaSnzu4O.js
  39. 0 0
      frontend/dist/assets/NotificationView-CeRAXi5F.js
  40. 0 0
      frontend/dist/assets/OrgView-DBmJYd-4.js
  41. 0 0
      frontend/dist/assets/PeriodBanner-DqtCk_tX.js
  42. 0 0
      frontend/dist/assets/PeriodListView-BeneuKGr.css
  43. 0 0
      frontend/dist/assets/PeriodListView-C6lJkC6c.js
  44. 0 0
      frontend/dist/assets/PeriodListView-Dh3VFwV9.js
  45. 0 0
      frontend/dist/assets/PeriodListView-DynDSbMv.css
  46. 0 0
      frontend/dist/assets/ScoringEditView-CmLeL416.js
  47. 0 0
      frontend/dist/assets/ScoringListView-CqWQ_EvB.js
  48. 0 1
      frontend/dist/assets/TeamOkrView-BCrZRD04.js
  49. 1 1
      frontend/dist/assets/auth-B53ss9y5.js
  50. 1 1
      frontend/dist/assets/index-BPukwgZW.js
  51. 0 0
      frontend/dist/assets/index-C1NeD1gi.js
  52. 1 1
      frontend/dist/index.html
  53. 1 0
      frontend/src/api/index.js
  54. 16 6
      frontend/src/views/period/PeriodListView.vue

+ 6 - 0
backend/src/main/java/com/yimeng/okr/controller/AssessmentPeriodController.java

@@ -2,6 +2,7 @@ package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
 import com.yimeng.okr.annotation.OperationLogAnnotation;
+import com.yimeng.okr.dto.PeriodReadinessDto;
 import com.yimeng.okr.entity.AssessmentPeriod;
 import com.yimeng.okr.enums.PeriodStatus;
 import com.yimeng.okr.enums.UserRole;
@@ -33,6 +34,11 @@ public class AssessmentPeriodController {
         return ApiResult.success(periodService.getById(id));
     }
 
+    @GetMapping("/{id}/readiness")
+    public ApiResult<PeriodReadinessDto> checkReadiness(@PathVariable Long id, @RequestParam String target) {
+        return ApiResult.success(periodService.checkReadiness(id, PeriodStatus.valueOf(target)));
+    }
+
     @GetMapping("/active")
     public ApiResult<AssessmentPeriod> getActive() {
         return ApiResult.success(periodService.getActivePeriod());

+ 16 - 0
backend/src/main/java/com/yimeng/okr/dto/PeriodReadinessDto.java

@@ -0,0 +1,16 @@
+package com.yimeng.okr.dto;
+
+import com.yimeng.okr.enums.PeriodStatus;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class PeriodReadinessDto {
+    private Long periodId;
+    private PeriodStatus currentStatus;
+    private PeriodStatus targetStatus;
+    private boolean ready;
+    private List<String> blockers = new ArrayList<>();
+}

+ 71 - 0
backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java

@@ -3,6 +3,7 @@ package com.yimeng.okr.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yimeng.okr.dto.PeriodReadinessDto;
 import com.yimeng.okr.entity.AssessmentPeriod;
 import com.yimeng.okr.entity.KpiConfig;
 import com.yimeng.okr.entity.KrKeyResult;
@@ -10,12 +11,14 @@ import com.yimeng.okr.entity.OkrObjective;
 import com.yimeng.okr.entity.SysUser;
 import com.yimeng.okr.enums.PeriodStatus;
 import com.yimeng.okr.enums.PeriodType;
+import com.yimeng.okr.enums.ScoreType;
 import com.yimeng.okr.event.NotificationEvent;
 import com.yimeng.okr.exception.BusinessException;
 import com.yimeng.okr.mapper.AssessmentPeriodMapper;
 import com.yimeng.okr.mapper.KpiConfigMapper;
 import com.yimeng.okr.mapper.KrKeyResultMapper;
 import com.yimeng.okr.mapper.OkrObjectiveMapper;
+import com.yimeng.okr.mapper.PerformanceScoreMapper;
 import com.yimeng.okr.mapper.SysUserMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -38,6 +41,7 @@ public class AssessmentPeriodService {
     private final OkrObjectiveMapper objectiveMapper;
     private final KrKeyResultMapper krMapper;
     private final KpiConfigMapper kpiConfigMapper;
+    private final PerformanceScoreMapper scoreMapper;
     private final ApplicationEventPublisher publisher;
     private final ObjectMapper objectMapper = new ObjectMapper();
 
@@ -94,6 +98,8 @@ public class AssessmentPeriodService {
                     current.getLabel(), targetStatus.getLabel(), hint));
         }
 
+        requireReadyForTransition(period, targetStatus);
+
         period.setStatus(targetStatus);
         periodMapper.updateById(period);
         if (targetStatus == PeriodStatus.ARCHIVED) {
@@ -104,6 +110,71 @@ public class AssessmentPeriodService {
         notifyPeriodTransition(period, targetStatus);
     }
 
+    public PeriodReadinessDto checkReadiness(Long id, PeriodStatus targetStatus) {
+        AssessmentPeriod period = getById(id);
+        PeriodReadinessDto readiness = new PeriodReadinessDto();
+        readiness.setPeriodId(period.getId());
+        readiness.setCurrentStatus(period.getStatus());
+        readiness.setTargetStatus(targetStatus);
+
+        List<String> blockers = readiness.getBlockers();
+        if (!isValidTransition(period.getStatus(), targetStatus) && period.getStatus() != targetStatus) {
+            blockers.add("当前状态不能直接转为" + targetStatus.getLabel());
+        } else {
+            collectTransitionBlockers(period.getId(), targetStatus, blockers);
+        }
+        readiness.setReady(blockers.isEmpty());
+        return readiness;
+    }
+
+    private void requireReadyForTransition(AssessmentPeriod period, PeriodStatus targetStatus) {
+        List<String> blockers = new ArrayList<>();
+        collectTransitionBlockers(period.getId(), targetStatus, blockers);
+        if (!blockers.isEmpty()) {
+            throw new BusinessException("周期尚未满足进入【" + targetStatus.getLabel() + "】条件:" + String.join(";", blockers));
+        }
+    }
+
+    private void collectTransitionBlockers(Long periodId, PeriodStatus targetStatus, List<String> blockers) {
+        if (targetStatus == PeriodStatus.EXECUTING || targetStatus == PeriodStatus.ASSESSING) {
+            long pendingOkrs = objectiveMapper.selectCount(
+                    new LambdaQueryWrapper<OkrObjective>()
+                            .eq(OkrObjective::getPeriodId, periodId)
+                            .in(OkrObjective::getStatus, List.of("DRAFT", "SUBMITTED")));
+            if (pendingOkrs > 0) {
+                blockers.add("存在 " + pendingOkrs + " 条待审核OKR");
+            }
+
+            long unlockedKpis = kpiConfigMapper.selectCount(
+                    new LambdaQueryWrapper<KpiConfig>()
+                            .eq(KpiConfig::getPeriodId, periodId)
+                            .isNull(KpiConfig::getLockedAt));
+            if (unlockedKpis > 0) {
+                blockers.add("存在 " + unlockedKpis + " 份未锁定KPI");
+            }
+        }
+
+        if (targetStatus == PeriodStatus.ARCHIVED) {
+            long finalScores = scoreMapper.selectCount(
+                    new LambdaQueryWrapper<com.yimeng.okr.entity.PerformanceScore>()
+                            .eq(com.yimeng.okr.entity.PerformanceScore::getPeriodId, periodId)
+                            .eq(com.yimeng.okr.entity.PerformanceScore::getType, ScoreType.FINAL));
+            if (finalScores == 0) {
+                blockers.add("暂无最终评分");
+                return;
+            }
+
+            long unconfirmedScores = scoreMapper.selectCount(
+                    new LambdaQueryWrapper<com.yimeng.okr.entity.PerformanceScore>()
+                            .eq(com.yimeng.okr.entity.PerformanceScore::getPeriodId, periodId)
+                            .eq(com.yimeng.okr.entity.PerformanceScore::getType, ScoreType.FINAL)
+                            .isNull(com.yimeng.okr.entity.PerformanceScore::getConfirmedAt));
+            if (unconfirmedScores > 0) {
+                blockers.add("存在 " + unconfirmedScores + " 条未确认最终评分");
+            }
+        }
+    }
+
     private void notifyPeriodTransition(AssessmentPeriod period, PeriodStatus target) {
         String title;
         String content;

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/AlignmentView-DKI5FD_z.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/DimensionConfigView-MTWy69Yi.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/FeedbackView-B_WRJxa3.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/HistoryView-DHvaPeyY.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/KpiTemplateView-Cv00xKsI.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/LogView-TQOFYcmz.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/LoginView-Di359Fkb.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
backend/src/main/resources/static/assets/MainLayout-LaLCHCLE.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/MyOkrView-eEkrPM1k.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/MyResultView-BaSnzu4O.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/NotificationView-CeRAXi5F.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/OrgView-DBmJYd-4.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/PeriodBanner-DqtCk_tX.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/PeriodListView-BeneuKGr.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/PeriodListView-C6lJkC6c.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/PeriodListView-Dh3VFwV9.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/PeriodListView-DynDSbMv.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/ScoringEditView-CmLeL416.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/ScoringListView-CqWQ_EvB.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
backend/src/main/resources/static/assets/TeamOkrView-BCrZRD04.js


+ 1 - 1
backend/src/main/resources/static/assets/auth-Cv0WaWtQ.js → backend/src/main/resources/static/assets/auth-B53ss9y5.js

@@ -1 +1 @@
-import{A as t}from"./index-DzdxblQI.js";import{authApi as r}from"./index-BrgsZ4qv.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:e=>!!e.accessToken,isAdmin:e=>{var s;return((s=e.user)==null?void 0:s.role)==="SUPER_ADMIN"},isHrAdminOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN"].includes((s=e.user)==null?void 0:s.role)},isDeptLeaderOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((s=e.user)==null?void 0:s.role)},roleLabel:e=>{var o;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(o=e.user)==null?void 0:o.role]||"员工"},roleColor:e=>{var o;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(o=e.user)==null?void 0:o.role]||"#409eff"}},actions:{async login(e,s){const o=await r.login({username:e,password:s});this.accessToken=o.accessToken,this.refreshToken=o.refreshToken,this.user=o.user,localStorage.setItem("accessToken",o.accessToken),localStorage.setItem("refreshToken",o.refreshToken)},async fetchUser(){try{this.user=await r.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};
+import{A as t}from"./index-BPukwgZW.js";import{authApi as r}from"./index-C1NeD1gi.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:e=>!!e.accessToken,isAdmin:e=>{var s;return((s=e.user)==null?void 0:s.role)==="SUPER_ADMIN"},isHrAdminOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN"].includes((s=e.user)==null?void 0:s.role)},isDeptLeaderOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((s=e.user)==null?void 0:s.role)},roleLabel:e=>{var o;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(o=e.user)==null?void 0:o.role]||"员工"},roleColor:e=>{var o;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(o=e.user)==null?void 0:o.role]||"#409eff"}},actions:{async login(e,s){const o=await r.login({username:e,password:s});this.accessToken=o.accessToken,this.refreshToken=o.refreshToken,this.user=o.user,localStorage.setItem("accessToken",o.accessToken),localStorage.setItem("refreshToken",o.refreshToken)},async fetchUser(){try{this.user=await r.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};

Файловите разлики са ограничени, защото са твърде много
+ 1 - 1
backend/src/main/resources/static/assets/index-BPukwgZW.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
backend/src/main/resources/static/assets/index-C1NeD1gi.js


+ 1 - 1
backend/src/main/resources/static/index.html

@@ -5,7 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>医梦AI · OKR绩效</title>
   <link rel="icon" type="image/png" href="/logo-small.png" />
-  <script type="module" crossorigin src="/assets/index-DzdxblQI.js"></script>
+  <script type="module" crossorigin src="/assets/index-BPukwgZW.js"></script>
   <link rel="stylesheet" crossorigin href="/assets/index-CT17CCWP.css">
 </head>
 <body>

+ 63 - 0
backend/src/test/java/com/yimeng/okr/service/AssessmentPeriodServiceTest.java

@@ -0,0 +1,63 @@
+package com.yimeng.okr.service;
+
+import com.yimeng.okr.entity.AssessmentPeriod;
+import com.yimeng.okr.enums.PeriodStatus;
+import com.yimeng.okr.exception.BusinessException;
+import com.yimeng.okr.mapper.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class AssessmentPeriodServiceTest {
+
+    @Mock AssessmentPeriodMapper periodMapper;
+    @Mock SysUserMapper userMapper;
+    @Mock OkrObjectiveMapper objectiveMapper;
+    @Mock KrKeyResultMapper krMapper;
+    @Mock KpiConfigMapper kpiConfigMapper;
+    @Mock PerformanceScoreMapper scoreMapper;
+    @Mock ApplicationEventPublisher publisher;
+    @InjectMocks AssessmentPeriodService periodService;
+
+    @Test
+    void transitionToExecutingRejectsPendingOkrReview() {
+        AssessmentPeriod period = period(1L, PeriodStatus.OKR_ALIGN);
+        when(periodMapper.selectById(1L)).thenReturn(period);
+        when(objectiveMapper.selectCount(any())).thenReturn(1L);
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> periodService.transitionStatus(1L, PeriodStatus.EXECUTING));
+
+        assertTrue(ex.getMessage().contains("待审核OKR"));
+        verify(periodMapper, never()).updateById(any());
+    }
+
+    @Test
+    void transitionToArchivedRejectsUnconfirmedFinalScores() {
+        AssessmentPeriod period = period(2L, PeriodStatus.ASSESSING);
+        when(periodMapper.selectById(2L)).thenReturn(period);
+        when(scoreMapper.selectCount(any())).thenReturn(1L, 1L);
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> periodService.transitionStatus(2L, PeriodStatus.ARCHIVED));
+
+        assertTrue(ex.getMessage().contains("未确认最终评分"));
+        verify(periodMapper, never()).updateById(any());
+    }
+
+    private AssessmentPeriod period(Long id, PeriodStatus status) {
+        AssessmentPeriod period = new AssessmentPeriod();
+        period.setId(id);
+        period.setName("2026年6月");
+        period.setStatus(status);
+        return period;
+    }
+}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/AlignmentView-DKI5FD_z.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/DimensionConfigView-MTWy69Yi.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/FeedbackView-B_WRJxa3.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/HistoryView-DHvaPeyY.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/KpiTemplateView-Cv00xKsI.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/LogView-TQOFYcmz.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/LoginView-Di359Fkb.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
frontend/dist/assets/MainLayout-LaLCHCLE.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/MyOkrView-eEkrPM1k.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/MyResultView-BaSnzu4O.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/NotificationView-CeRAXi5F.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/OrgView-DBmJYd-4.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/PeriodBanner-DqtCk_tX.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/PeriodListView-BeneuKGr.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/PeriodListView-C6lJkC6c.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/PeriodListView-Dh3VFwV9.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/PeriodListView-DynDSbMv.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/ScoringEditView-CmLeL416.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/ScoringListView-CqWQ_EvB.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
frontend/dist/assets/TeamOkrView-BCrZRD04.js


+ 1 - 1
frontend/dist/assets/auth-Cv0WaWtQ.js → frontend/dist/assets/auth-B53ss9y5.js

@@ -1 +1 @@
-import{A as t}from"./index-DzdxblQI.js";import{authApi as r}from"./index-BrgsZ4qv.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:e=>!!e.accessToken,isAdmin:e=>{var s;return((s=e.user)==null?void 0:s.role)==="SUPER_ADMIN"},isHrAdminOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN"].includes((s=e.user)==null?void 0:s.role)},isDeptLeaderOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((s=e.user)==null?void 0:s.role)},roleLabel:e=>{var o;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(o=e.user)==null?void 0:o.role]||"员工"},roleColor:e=>{var o;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(o=e.user)==null?void 0:o.role]||"#409eff"}},actions:{async login(e,s){const o=await r.login({username:e,password:s});this.accessToken=o.accessToken,this.refreshToken=o.refreshToken,this.user=o.user,localStorage.setItem("accessToken",o.accessToken),localStorage.setItem("refreshToken",o.refreshToken)},async fetchUser(){try{this.user=await r.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};
+import{A as t}from"./index-BPukwgZW.js";import{authApi as r}from"./index-C1NeD1gi.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:e=>!!e.accessToken,isAdmin:e=>{var s;return((s=e.user)==null?void 0:s.role)==="SUPER_ADMIN"},isHrAdminOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN"].includes((s=e.user)==null?void 0:s.role)},isDeptLeaderOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((s=e.user)==null?void 0:s.role)},roleLabel:e=>{var o;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(o=e.user)==null?void 0:o.role]||"员工"},roleColor:e=>{var o;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(o=e.user)==null?void 0:o.role]||"#409eff"}},actions:{async login(e,s){const o=await r.login({username:e,password:s});this.accessToken=o.accessToken,this.refreshToken=o.refreshToken,this.user=o.user,localStorage.setItem("accessToken",o.accessToken),localStorage.setItem("refreshToken",o.refreshToken)},async fetchUser(){try{this.user=await r.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};

Файловите разлики са ограничени, защото са твърде много
+ 1 - 1
frontend/dist/assets/index-BPukwgZW.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/assets/index-C1NeD1gi.js


+ 1 - 1
frontend/dist/index.html

@@ -5,7 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>医梦AI · OKR绩效</title>
   <link rel="icon" type="image/png" href="/logo-small.png" />
-  <script type="module" crossorigin src="/assets/index-DzdxblQI.js"></script>
+  <script type="module" crossorigin src="/assets/index-BPukwgZW.js"></script>
   <link rel="stylesheet" crossorigin href="/assets/index-CT17CCWP.css">
 </head>
 <body>

+ 1 - 0
frontend/src/api/index.js

@@ -69,6 +69,7 @@ export const periodApi = {
   batchCreate: data => api.post('/periods/batch', data).then(r => r.data),
   update: (id, data) => api.put(`/periods/${id}`, data),
   delete: id => api.delete(`/periods/${id}`),
+  checkReadiness: (id, target) => api.get(`/periods/${id}/readiness`, { params: { target } }).then(r => r.data),
   transition: (id, status) => api.put(`/periods/${id}/status`, { status })
 }
 

+ 16 - 6
frontend/src/views/period/PeriodListView.vue

@@ -68,10 +68,10 @@
               </div>
               <div class="card-actions">
                 <template v-if="auth.isAdmin">
-                  <el-button v-if="p.status === 'EXECUTING'" type="warning" size="small" plain @click="transition(p.id, 'OKR_ALIGN')" round>回退</el-button>
-                  <el-button v-if="p.status === 'EXECUTING'" type="primary" size="small" @click="transition(p.id, 'ASSESSING')" round>进入考评</el-button>
-                  <el-button v-if="p.status === 'EXECUTING'" type="danger" size="small" plain @click="transition(p.id, 'ARCHIVED')" round>直接归档</el-button>
-                  <el-button v-if="p.status === 'ASSESSING'" type="warning" @click="transition(p.id, 'ARCHIVED')" round>结束并归档</el-button>
+                  <el-button v-if="p.status === 'EXECUTING'" type="warning" size="small" plain :loading="transitioning === p.id" @click="transition(p.id, 'OKR_ALIGN')" round>回退</el-button>
+                  <el-button v-if="p.status === 'EXECUTING'" type="primary" size="small" :loading="transitioning === p.id" @click="transition(p.id, 'ASSESSING')" round>进入考评</el-button>
+                  <el-button v-if="p.status === 'EXECUTING'" type="danger" size="small" plain :loading="transitioning === p.id" @click="transition(p.id, 'ARCHIVED')" round>直接归档</el-button>
+                  <el-button v-if="p.status === 'ASSESSING'" type="warning" :loading="transitioning === p.id" @click="transition(p.id, 'ARCHIVED')" round>结束并归档</el-button>
                 </template>
                 <el-button v-if="p.status === 'ASSESSING'" type="warning" size="small" @click="$router.push({ path: '/scores/my', query: { periodId: p.id } })" round>去自评</el-button>
               </div>
@@ -95,7 +95,7 @@
               </div>
               <div class="card-actions">
                 <template v-if="auth.isAdmin">
-                  <el-button type="warning" size="small" @click="transition(p.id, 'EXECUTING')" round>锁定并开始执行</el-button>
+                  <el-button type="warning" size="small" :loading="transitioning === p.id" @click="transition(p.id, 'EXECUTING')" round>锁定并开始执行</el-button>
                 </template>
                 <el-button type="primary" size="small" @click="$router.push({ path: '/okr/my', query: { periodId: p.id } })" round>去填写 OKR</el-button>
               </div>
@@ -118,7 +118,7 @@
                 <div class="card-date">{{ p.startDate }} ~ {{ p.endDate }}</div>
               </div>
               <div class="card-actions" v-if="auth.isAdmin">
-                <el-button type="primary" size="small" @click="transition(p.id, 'OKR_ALIGN')" round>发布 OKR 对齐</el-button>
+                <el-button type="primary" size="small" :loading="transitioning === p.id" @click="transition(p.id, 'OKR_ALIGN')" round>发布 OKR 对齐</el-button>
                 <el-button type="danger" size="small" plain @click="handleDelete(p.id)" round>删除</el-button>
               </div>
             </div>
@@ -319,7 +319,17 @@ async function transition(id, status) {
   if (transitioning.value) return
   transitioning.value = id
   try {
+    const readiness = await periodApi.checkReadiness(id, status)
+    if (!readiness.ready) {
+      await ElMessageBox.alert(
+        readiness.blockers.map(item => `· ${item}`).join('<br>'),
+        '暂不能切换周期状态',
+        { dangerouslyUseHTMLString: true, confirmButtonText: '知道了', type: 'warning' }
+      )
+      return
+    }
     await periodApi.transition(id, status)
+    ElMessage.success('周期状态已更新')
     load()
   } catch {
     // error handled by interceptor

Някои файлове не бяха показани, защото твърде много файлове са промени