|
|
@@ -11,6 +11,47 @@
|
|
|
</div>
|
|
|
|
|
|
<div v-if="data.targetUser" class="scoring-body">
|
|
|
+ <section class="leader-peer-card">
|
|
|
+ <div class="leader-peer-card__header">
|
|
|
+ <div>
|
|
|
+ <div class="leader-peer-card__title">360 环评参考</div>
|
|
|
+ <div class="leader-peer-card__meta">评价信息仅本周期冻结 Leader 可见,不计入绩效总分。</div>
|
|
|
+ </div>
|
|
|
+ <el-tag effect="plain" :type="leaderSummary.canFinalize ? 'success' : 'warning'">
|
|
|
+ {{ leaderSummary.submittedCount }} 份有效评价
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-if="leaderSummary.status === 'PENDING_APPROVAL'">
|
|
|
+ <div class="leader-invitation-list">
|
|
|
+ <span v-for="item in leaderSummary.invitations" :key="item.invitationId">
|
|
|
+ {{ item.reviewerName }} · {{ relationLabel(item.relationType) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="leader-peer-actions">
|
|
|
+ <el-button type="primary" :loading="peerReviewSaving" @click="reviewPeerList(true)">批准并发送邀请</el-button>
|
|
|
+ <el-button :loading="peerReviewSaving" @click="reviewPeerList(false)">退回调整</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="leaderSummary.responses?.length">
|
|
|
+ <div class="peer-average-grid">
|
|
|
+ <div><span>目标贡献</span><strong>{{ formatPeerAverage(leaderSummary.averageGoalContributionScore) }}</strong></div>
|
|
|
+ <div><span>协作表现</span><strong>{{ formatPeerAverage(leaderSummary.averageCollaborationScore) }}</strong></div>
|
|
|
+ <div><span>担当意识</span><strong>{{ formatPeerAverage(leaderSummary.averageAccountabilityScore) }}</strong></div>
|
|
|
+ </div>
|
|
|
+ <div v-for="item in leaderSummary.responses" :key="item.reviewerId" class="leader-peer-response">
|
|
|
+ <div class="leader-peer-response__name">{{ item.reviewerName }}</div>
|
|
|
+ <div class="leader-peer-response__scores">
|
|
|
+ 目标贡献 {{ item.goalContributionScore }} · 协作表现 {{ item.collaborationScore }} · 担当意识 {{ item.accountabilityScore }}
|
|
|
+ </div>
|
|
|
+ <div><strong>做得好的:</strong>{{ item.strengths }}</div>
|
|
|
+ <div><strong>改进建议:</strong>{{ item.improvementSuggestion }}</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div v-else class="leader-peer-empty">尚未收到有效评价。</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
<div class="compare-grid-3__header">
|
|
|
<span>证据 · 员工自评</span>
|
|
|
<span class="compare-grid-3__self">自评分</span>
|
|
|
@@ -132,9 +173,11 @@
|
|
|
type="primary"
|
|
|
@click="handleSubmit"
|
|
|
:loading="submitting"
|
|
|
+ :disabled="!leaderSummary.canFinalize"
|
|
|
>
|
|
|
提交绩效考评
|
|
|
</el-button>
|
|
|
+ <span v-if="!data.superiorScore && !leaderSummary.canFinalize" class="finalize-gate-hint">至少收到 2 份有效评价后才可终评</span>
|
|
|
<template v-else>
|
|
|
<span class="status-label">
|
|
|
<span class="status-dot" :class="data.finalScore?.status === 'PUBLISHED' ? 'status-dot--active' : 'status-dot--warn'"></span>
|
|
|
@@ -157,8 +200,8 @@
|
|
|
<script setup>
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
import { useRoute } from 'vue-router'
|
|
|
-import { scoreApi, dimensionApi } from '../../api'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
+import { scoreApi, dimensionApi, peerReviewApi } from '../../api'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
const route = useRoute()
|
|
|
const data = ref({})
|
|
|
@@ -168,6 +211,8 @@ const form = reactive({ bonus: 0, penalty: 0, remark: '' })
|
|
|
const krScores = ref([])
|
|
|
const dimensions = ref([])
|
|
|
const dimScores = ref([])
|
|
|
+const leaderSummary = ref({ status: 'NOT_STARTED', submittedCount: 0, canFinalize: false, invitations: [], responses: [] })
|
|
|
+const peerReviewSaving = ref(false)
|
|
|
|
|
|
const okrRecords = computed(() => data.value.okrRecords || [])
|
|
|
const allKrs = computed(() => okrRecords.value.flatMap(objective =>
|
|
|
@@ -312,6 +357,10 @@ function selfKrScoreColor(i) {
|
|
|
|
|
|
async function loadPage() {
|
|
|
data.value = await scoreApi.getPage(route.query.periodId, route.params.userId)
|
|
|
+ const queue = await peerReviewApi.getLeaderQueue(route.query.periodId)
|
|
|
+ if (queue.some(item => item.subjectUserId === Number(route.params.userId))) {
|
|
|
+ leaderSummary.value = await peerReviewApi.getLeaderSummary(route.query.periodId, Number(route.params.userId))
|
|
|
+ }
|
|
|
const okrs = okrRecords.value
|
|
|
krScores.value = allKrs.value.map(kr => ({ title: kr.title, score: 0.6 }))
|
|
|
|
|
|
@@ -323,6 +372,33 @@ async function loadPage() {
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
+async function reviewPeerList(approved) {
|
|
|
+ let reason = ''
|
|
|
+ if (!approved) {
|
|
|
+ const result = await ElMessageBox.prompt('请说明需要员工调整名单的原因', '退回评价人名单', {
|
|
|
+ inputType: 'textarea',
|
|
|
+ inputPlaceholder: '例如:请补充一位熟悉项目交付过程的跨部门同事',
|
|
|
+ confirmButtonText: '确认退回',
|
|
|
+ cancelButtonText: '取消'
|
|
|
+ })
|
|
|
+ reason = result.value
|
|
|
+ }
|
|
|
+ peerReviewSaving.value = true
|
|
|
+ try {
|
|
|
+ await peerReviewApi.reviewRequest(leaderSummary.value.requestId, { approved, reason })
|
|
|
+ ElMessage.success(approved ? '名单已批准,邀请已发送' : '名单已退回员工调整')
|
|
|
+ await loadPage()
|
|
|
+ } finally { peerReviewSaving.value = false }
|
|
|
+}
|
|
|
+
|
|
|
+function relationLabel(type) {
|
|
|
+ return type === 'SAME_DEPARTMENT' ? '同部门' : '跨部门'
|
|
|
+}
|
|
|
+
|
|
|
+function formatPeerAverage(value) {
|
|
|
+ return Number(value || 0).toFixed(1)
|
|
|
+}
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
await loadPage()
|
|
|
})
|
|
|
@@ -372,6 +448,20 @@ function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进
|
|
|
.page { width: 100%; }
|
|
|
.review-workbench { background: var(--color-surface); }
|
|
|
.scoring-body { padding-bottom: 72px; }
|
|
|
+.leader-peer-card { padding: 18px 20px; border-bottom: 1px solid var(--table-border); background: var(--color-surface); color: var(--color-text); }
|
|
|
+.leader-peer-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
|
|
|
+.leader-peer-card__title { color: var(--color-text); font-size: 16px; font-weight: 700; }
|
|
|
+.leader-peer-card__meta { margin-top: 4px; color: var(--color-text-secondary); font-size: var(--font-size-sm); }
|
|
|
+.leader-invitation-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
|
+.leader-invitation-list span { padding: 7px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: var(--color-text); background: var(--color-surface-subtle); font-size: var(--font-size-sm); }
|
|
|
+.leader-peer-actions { margin-top: 14px; }
|
|
|
+.peer-average-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 12px; }
|
|
|
+.peer-average-grid div { display: flex; justify-content: space-between; padding: 10px 12px; background: var(--color-surface-tint-brand); border: 1px solid var(--color-border-soft); border-radius: var(--radius-sm); color: var(--color-text-secondary); }
|
|
|
+.peer-average-grid strong { color: var(--color-brand); }
|
|
|
+.leader-peer-response { padding: 12px 0; border-top: 1px solid var(--color-border-soft); color: var(--color-text-secondary); font-size: var(--font-size-sm); line-height: 1.7; }
|
|
|
+.leader-peer-response__name { color: var(--color-text); font-weight: 700; }
|
|
|
+.leader-peer-response__scores { color: var(--color-brand); font-weight: 600; }
|
|
|
+.leader-peer-empty { color: var(--color-text-secondary); font-size: var(--font-size-sm); }
|
|
|
.section-band--spaced { margin-top: 8px; border-top: 1px solid var(--table-border); }
|
|
|
.panel-empty { color: var(--color-text-muted); text-align: center; padding: 24px; font-size: var(--font-size-sm); }
|
|
|
.evidence-title { color: var(--color-text); font-weight: 500; }
|
|
|
@@ -407,6 +497,7 @@ function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进
|
|
|
padding: 12px 14px;
|
|
|
border-top: 1px solid var(--toolbar-border);
|
|
|
}
|
|
|
+.finalize-gate-hint { color: var(--color-warning); font-size: var(--font-size-sm); font-weight: 600; }
|
|
|
|
|
|
@media (max-width: 1279px) {
|
|
|
.compare-grid-3,
|
|
|
@@ -418,5 +509,6 @@ function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进
|
|
|
.remark-row { grid-template-columns: 1fr; }
|
|
|
.review-summary-bar { position: static; flex-wrap: wrap; }
|
|
|
.summary-diff { margin-left: 0; width: 100%; }
|
|
|
+ .peer-average-grid { grid-template-columns: 1fr; }
|
|
|
}
|
|
|
</style>
|