|
|
@@ -1,193 +1,175 @@
|
|
|
<template>
|
|
|
<div class="app-page okr-workbench">
|
|
|
- <div class="page-hero">
|
|
|
- <div class="hero-info">
|
|
|
- <h2>我的 OKR</h2>
|
|
|
- <p>本周期目标与关键结果</p>
|
|
|
- </div>
|
|
|
+ <!-- v2 工具栏顶栏(替代旧版标题区)-->
|
|
|
+ <div class="data-toolbar">
|
|
|
+ <span class="data-toolbar__title">我的 OKR</span>
|
|
|
+ <template v-if="period">
|
|
|
+ <span class="data-toolbar__sep">|</span>
|
|
|
+ <span class="data-toolbar__meta">{{ period.name }}</span>
|
|
|
+ <span class="status-label">
|
|
|
+ <span class="status-label__dot" :class="periodStatusDotClass"></span>
|
|
|
+ {{ periodStatusLabel }}
|
|
|
+ </span>
|
|
|
+ <span class="data-toolbar__spacer"></span>
|
|
|
+ <span class="data-toolbar__meta" v-if="daysLeft !== null">距周期结束 {{ daysLeft }} 天</span>
|
|
|
+ <el-button v-if="period?.status === 'OKR_ALIGN' && myOkr" size="small" @click="startEdit">编辑 OKR</el-button>
|
|
|
+ <el-button v-else-if="period?.status === 'OKR_ALIGN' && !myOkr && canSubmit" type="primary" size="small" @click="openCreateForm">制定 OKR</el-button>
|
|
|
+ <el-button v-else-if="period?.status === 'ASSESSING'" type="warning" size="small" @click="$router.push('/scores/my')">去完成自评</el-button>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
|
|
|
<PeriodBanner v-if="periods.length" :periods="periods" v-model="selectedPeriodId" />
|
|
|
- <div v-if="!selectedPeriodId" class="empty-big">暂无进行中的考核周期</div>
|
|
|
+ <div v-if="!selectedPeriodId" class="okr-empty">暂无进行中的考核周期</div>
|
|
|
|
|
|
<template v-else>
|
|
|
- <div class="state-banner" :class="'banner-'+period?.status">
|
|
|
- <span class="banner-icon">{{ stateIcon }}</span>
|
|
|
- <span class="banner-text">{{ stateBanner }}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- ByteDance rules + Position template -->
|
|
|
- <div class="rules-card okr-standard-panel med-panel">
|
|
|
- <div class="rules-title">OKR 规则 · {{ auth.user?.position || '通用岗位' }}</div>
|
|
|
- <div class="rules-grid">
|
|
|
- <div class="rule-item"><b>O</b>:定性方向,15-30字,不含数字指标</div>
|
|
|
- <div class="rule-item"><b>KR</b>:2~5条,可量化结果,权重之和=100</div>
|
|
|
- <div class="rule-item"><b>对齐</b>:下级O必须承接上级O方向</div>
|
|
|
- <div class="rule-item"><b>打分</b>:逐条 KR 0-1 分,以交付结果为依据</div>
|
|
|
+ <!-- 我的 OKR 表格视图(OKR_ALIGN 有 myOkr / EXECUTING)-->
|
|
|
+ <div v-if="(period?.status === 'OKR_ALIGN' || period?.status === 'EXECUTING') && myOkr" class="data-table">
|
|
|
+ <div class="data-table__header" style="grid-template-columns: 36px 1fr 56px 72px 120px 80px;">
|
|
|
+ <span></span>
|
|
|
+ <span>关键结果</span>
|
|
|
+ <span>权重</span>
|
|
|
+ <span>进度</span>
|
|
|
+ <span>当前/目标</span>
|
|
|
+ <span>状态</span>
|
|
|
</div>
|
|
|
- <div v-if="positionTemplate" class="position-ref">
|
|
|
- <div class="pr-title">你的岗位指标参考({{ positionTemplate.name }})</div>
|
|
|
- <div class="pr-items">
|
|
|
- <el-tag v-for="(item, i) in parseKpiItems(positionTemplate.itemsJson)" :key="i"
|
|
|
- size="small" effect="plain" type="info" round>
|
|
|
- {{ item.name }}(满分{{ item.maxScore }})
|
|
|
- </el-tag>
|
|
|
- </div>
|
|
|
- <div class="pr-hint">OKR 目标应与这些 KPI 指标方向一致</div>
|
|
|
+ <div v-for="(kr, i) in myOkr.keyResults" :key="kr.id"
|
|
|
+ class="data-table__row data-table__row--clickable"
|
|
|
+ style="grid-template-columns: 36px 1fr 56px 72px 120px 80px;"
|
|
|
+ @click="openKrDrawer(kr)">
|
|
|
+ <span style="color: var(--color-accent); font-weight: 700;">KR{{ i+1 }}</span>
|
|
|
+ <span style="color: var(--color-text);">
|
|
|
+ {{ kr.title }}
|
|
|
+ <span v-if="kr.sourceKrId" class="kr-source-tag">承接上级</span>
|
|
|
+ <span v-if="kr.assignedToName" class="kr-source-tag">指派给 {{ kr.assignedToName }}</span>
|
|
|
+ </span>
|
|
|
+ <span>{{ kr.weight }}</span>
|
|
|
+ <span style="color: var(--color-brand); font-weight: 600;">{{ krPercent(kr) }}%</span>
|
|
|
+ <span style="color: var(--color-text-secondary);">{{ kr.currentValue }}/{{ kr.targetValue }}</span>
|
|
|
+ <span class="status-label" style="font-size: 11px;">
|
|
|
+ <span class="status-dot" :class="krStatusDotClass(kr)"></span>
|
|
|
+ {{ krStatusLabel(kr.status) }}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- OKR_ALIGN Phase -->
|
|
|
- <template v-if="period?.status === 'OKR_ALIGN'">
|
|
|
- <div v-if="myOkr" class="my-okr-section">
|
|
|
- <div class="section-head">
|
|
|
- <span class="section-badge personal">我的 OKR</span>
|
|
|
- <span class="section-title">{{ myOkr.title }}</span>
|
|
|
- <el-tag :type="myOkr.status === 'APPROVED' ? 'success' : myOkr.status === 'SUBMITTED' ? 'warning' : 'danger'"
|
|
|
- size="large" effect="dark" round>
|
|
|
- {{ myOkr.status === 'APPROVED' ? '已通过' : myOkr.status === 'SUBMITTED' ? '待审核' : '已驳回' }}
|
|
|
- </el-tag>
|
|
|
- <el-button v-if="period?.status === 'OKR_ALIGN'" size="small" type="primary" plain @click="startEdit" round>
|
|
|
- 编辑 OKR
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- <div v-if="myOkr.alignedObjectives?.length" class="aligned-hint">
|
|
|
- 已对齐上级:<el-tag v-for="a in myOkr.alignedObjectives" :key="a.id" size="small" effect="plain">{{ a.userName }}: {{ truncate(a.title, 35) }}</el-tag>
|
|
|
- </div>
|
|
|
- <div class="okr-card" :class="'card-'+myOkr.status">
|
|
|
- <div v-for="(kr, i) in myOkr.keyResults" :key="kr.id" class="kr-display">
|
|
|
- <div class="kr-display-num">KR{{ i+1 }}</div>
|
|
|
- <div class="kr-display-text">{{ kr.title }}</div>
|
|
|
- <div class="kr-display-meta">
|
|
|
- <span class="kr-target">目标 {{ kr.targetValue }}</span>
|
|
|
- <el-tag v-if="kr.sourceKrId" size="small" effect="plain" type="info" round>承接上级</el-tag>
|
|
|
- <el-tag v-if="kr.assignedToName" size="small" effect="plain" type="warning" round>指派给 {{ kr.assignedToName }}</el-tag>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-if="myOkr.status === 'REJECTED'" class="rejected-hint">
|
|
|
- 你的 OKR 已被驳回,请修改后重新提交。
|
|
|
- <el-button type="primary" size="small" @click="startEdit" round style="margin-left:12px">修改并重新提交</el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <!-- OKR 被驳回提示 -->
|
|
|
+ <div v-if="period?.status === 'OKR_ALIGN' && myOkr?.status === 'REJECTED'" class="state-banner banner-REJECTED">
|
|
|
+ 你的 OKR 已被驳回,请修改后重新提交。
|
|
|
+ <el-button type="primary" size="small" @click="startEdit" style="margin-left:12px">修改并重新提交</el-button>
|
|
|
+ </div>
|
|
|
|
|
|
+ <!-- OKR_ALIGN 空态:无 myOkr -->
|
|
|
+ <template v-if="period?.status === 'OKR_ALIGN' && !myOkr">
|
|
|
+ <div v-if="!canSubmit" class="state-banner banner-waiting">
|
|
|
+ 等待上级先制定 OKR · {{ superiorName }} 完成并审核通过后,你可对齐并提交。
|
|
|
+ </div>
|
|
|
<template v-else>
|
|
|
- <div v-if="!canSubmit" class="hint-card waiting">
|
|
|
- <div class="hint-icon"><el-icon :size="28"><Clock /></el-icon></div>
|
|
|
- <div class="hint-content">
|
|
|
- <div class="hint-title">等待上级先制定 OKR</div>
|
|
|
- <div class="hint-desc">{{ superiorName }} 需要先完成 OKR 制定并通过审核,然后你才能对齐并提交自己的 OKR。</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <template v-else>
|
|
|
- <div v-if="superiorOkrs.length" class="section">
|
|
|
- <div class="section-head">
|
|
|
- <span class="section-badge superior">上级</span>
|
|
|
- <span class="section-title">{{ superiorName }} 的 OKR</span>
|
|
|
- <span class="section-hint">你的 O 必须承接上级 O 的核心方向</span>
|
|
|
+ <!-- 上级 OKR 表格化(对齐参考)-->
|
|
|
+ <div v-if="superiorOkrs.length" style="margin-top: 20px;">
|
|
|
+ <div class="section-band">上级 OKR · 对齐参考</div>
|
|
|
+ <div class="data-table">
|
|
|
+ <div class="data-table__header" style="grid-template-columns: 36px 1fr 120px;">
|
|
|
+ <span></span>
|
|
|
+ <span>关键结果</span>
|
|
|
+ <span>指派</span>
|
|
|
</div>
|
|
|
- <div class="okr-grid">
|
|
|
- <div v-for="o in superiorOkrs" :key="o.id" class="okr-card superior-card">
|
|
|
- <div class="okr-card-head">
|
|
|
- <span class="o-tag superior">O</span>
|
|
|
- <span class="o-title">{{ o.title }}</span>
|
|
|
- </div>
|
|
|
- <div v-for="(kr, i) in o.keyResults" :key="kr.id"
|
|
|
- class="mini-kr" :class="{ 'mine': kr.assignedTo === auth.user?.id }">
|
|
|
- <span class="mini-kr-num">KR{{ i+1 }}</span>
|
|
|
- <span class="mini-kr-text">{{ kr.title }}</span>
|
|
|
- <el-tag v-if="kr.assignedTo === auth.user?.id" size="small" type="warning" effect="plain" round>指派给我</el-tag>
|
|
|
- <span class="mini-val">{{ kr.targetValue }}</span>
|
|
|
- </div>
|
|
|
+ <template v-for="o in superiorOkrs" :key="o.id">
|
|
|
+ <div v-for="(kr, i) in o.keyResults" :key="kr.id"
|
|
|
+ class="data-table__row"
|
|
|
+ style="grid-template-columns: 36px 1fr 120px;">
|
|
|
+ <span style="color: var(--color-accent); font-weight: 700;">KR{{ i+1 }}</span>
|
|
|
+ <span style="color: var(--color-text);">{{ kr.title }}</span>
|
|
|
+ <span>
|
|
|
+ <span v-if="kr.assignedTo === auth.user?.id" class="kr-source-tag kr-source-tag--mine">指派给我</span>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-else class="hint-card top-level">
|
|
|
- <div class="hint-icon">
|
|
|
- <el-icon :size="28"><Aim /></el-icon>
|
|
|
- </div>
|
|
|
- <div class="hint-content">
|
|
|
- <div class="hint-title">你是最高层级,制定公司级 OKR</div>
|
|
|
- <div class="hint-desc">你的目标将决定下属的 OKR 方向。1个O + 3条KR,团队成员将基于此对齐。</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="action-row">
|
|
|
- <el-button type="primary" size="large" @click="openCreateForm" round>制定我的 OKR</el-button>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
- </template>
|
|
|
+ </div>
|
|
|
+ <div v-else class="state-banner banner-top-level">
|
|
|
+ 你是最高层级 · 制定公司级 OKR(1个O + 3条KR,团队将基于此对齐)
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</template>
|
|
|
|
|
|
- <!-- EXECUTING Phase -->
|
|
|
- <template v-if="period?.status === 'EXECUTING' && myOkr">
|
|
|
- <div class="section">
|
|
|
- <div class="section-head">
|
|
|
- <span class="section-badge personal">我的 OKR</span>
|
|
|
- <span class="section-title">{{ myOkr.title }}</span>
|
|
|
- <el-tag type="success" size="large" effect="dark" round>已锁定</el-tag>
|
|
|
+ <!-- EXECUTING 锁定提示 -->
|
|
|
+ <div v-if="period?.status === 'EXECUTING' && myOkr" class="state-banner banner-EXECUTING" style="margin-top: 12px;">
|
|
|
+ OKR 已锁定 · 仅可更新进度和标记阻塞,点击 KR 行在抽屉中更新。
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 待审核 OKR 表格化 -->
|
|
|
+ <div v-if="pendingReviews.length" style="margin-top: 20px;">
|
|
|
+ <div class="section-band">待审核 OKR · {{ pendingReviews.length }} 条</div>
|
|
|
+ <div class="data-table">
|
|
|
+ <div class="data-table__header" style="grid-template-columns: 36px 1fr 100px 120px;">
|
|
|
+ <span></span>
|
|
|
+ <span>关键结果</span>
|
|
|
+ <span>提交人</span>
|
|
|
+ <span>操作</span>
|
|
|
</div>
|
|
|
- <div class="rule-hint">OKR 已锁定,仅可更新进度和标记阻塞,不可修改目标与关键结果。</div>
|
|
|
- <div class="okr-card card-APPROVED" style="margin-top:12px">
|
|
|
- <div v-for="(kr, i) in myOkr.keyResults" :key="kr.id" class="kr-progress-row">
|
|
|
- <div class="kr-progress-head">
|
|
|
- <span class="kr-progress-num">KR{{ i+1 }}</span>
|
|
|
- <span class="kr-progress-text">{{ kr.title }}</span>
|
|
|
- <span class="kr-progress-val">{{ kr.currentValue }}/{{ kr.targetValue }}</span>
|
|
|
- </div>
|
|
|
- <el-progress :percentage="krPercent(kr)" :color="krColor(kr.status)" :stroke-width="8" />
|
|
|
- <div class="kr-progress-ctrl">
|
|
|
- <span class="kr-status-tag">{{ krStatusLabel(kr.status) }}</span>
|
|
|
- <el-input-number v-model="kr._newValue" :min="0" :max="kr.targetValue*2" size="small" controls-position="right" style="width:100px" />
|
|
|
- <el-select v-model="kr._newStatus" size="small" style="width:100px">
|
|
|
- <el-option label="进行中" value="IN_PROGRESS" /><el-option label="已完成" value="COMPLETED" />
|
|
|
- <el-option label="阻塞" value="BLOCKED" /><el-option label="未开始" value="NOT_STARTED" />
|
|
|
- </el-select>
|
|
|
- <el-button size="small" type="primary" @click="updateProgress(kr)" round :disabled="kr.status === 'COMPLETED'">更新</el-button>
|
|
|
- </div>
|
|
|
- <div v-if="kr.status === 'BLOCKED'" class="blocked-hint">
|
|
|
- 阻塞项,请说明原因与所需支持。上级已收到提醒。
|
|
|
- </div>
|
|
|
+ <template v-for="o in pendingReviews" :key="o.id">
|
|
|
+ <div v-for="(kr, i) in o.keyResults" :key="kr.id"
|
|
|
+ class="data-table__row"
|
|
|
+ style="grid-template-columns: 36px 1fr 100px 120px;">
|
|
|
+ <span style="color: var(--color-accent); font-weight: 700;">KR{{ i+1 }}</span>
|
|
|
+ <span style="color: var(--color-text);">{{ kr.title }}</span>
|
|
|
+ <span v-if="i === 0" :rowspan="o.keyResults.length" style="color: var(--color-text-secondary);">{{ o.userName }}</span>
|
|
|
+ <span v-else></span>
|
|
|
+ <span v-if="i === 0">
|
|
|
+ <el-button size="small" type="success" @click="doReview(o.id, true)">通过</el-button>
|
|
|
+ <el-button size="small" type="danger" @click="doReview(o.id, false)">驳回</el-button>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <div v-if="period?.status === 'ASSESSING'" class="action-row">
|
|
|
- <el-button type="warning" size="large" @click="$router.push('/scores/my')" round>去完成绩效自评</el-button>
|
|
|
</div>
|
|
|
+ </template>
|
|
|
|
|
|
- <!-- Pending Reviews -->
|
|
|
- <div v-if="pendingReviews.length" class="section">
|
|
|
- <div class="section-head">
|
|
|
- <span class="section-badge review">{{ pendingReviews.length }}</span>
|
|
|
- <span class="section-title">待审核 OKR</span>
|
|
|
- <span class="section-hint">请优先校验方向对齐度,其次校验 KR 合理性</span>
|
|
|
+ <!-- 侧边抽屉:KR 详情 / 进度更新 -->
|
|
|
+ <el-drawer v-model="drawerVisible" title="关键结果详情" size="420px" direction="rtl">
|
|
|
+ <div v-if="drawerKr" class="side-drawer__content">
|
|
|
+ <div class="uppercase-label">关键结果</div>
|
|
|
+ <div style="margin: 4px 0 16px; font-weight: 600; color: var(--color-text);">{{ drawerKr.title }}</div>
|
|
|
+
|
|
|
+ <div class="metric-bar">
|
|
|
+ <div class="metric-bar__head">
|
|
|
+ <span class="metric-bar__label">当前进度</span>
|
|
|
+ <span class="metric-bar__value">{{ drawerKr.currentValue }} / {{ drawerKr.targetValue }}
|
|
|
+ <span class="metric-bar__value--muted">({{ krPercent(drawerKr) }}%)</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="metric-bar__track">
|
|
|
+ <div class="metric-bar__fill" :style="{ width: krPercent(drawerKr) + '%', background: 'var(--color-accent)' }"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="okr-grid">
|
|
|
- <div v-for="o in pendingReviews" :key="o.id" class="okr-card review-card">
|
|
|
- <div class="okr-card-head">
|
|
|
- <span class="o-tag personal">O</span>
|
|
|
- <span class="o-title">{{ o.title }}</span>
|
|
|
- <el-tag effect="plain" type="warning" round>{{ o.userName }}</el-tag>
|
|
|
- </div>
|
|
|
- <div v-if="o.alignedObjectives?.length" class="aligned-hint">
|
|
|
- 对齐:<el-tag v-for="a in o.alignedObjectives" :key="a.id" size="small" effect="plain">{{ a.userName }}: {{ truncate(a.title, 25) }}</el-tag>
|
|
|
- </div>
|
|
|
- <div v-for="(kr, i) in o.keyResults" :key="kr.id" class="review-kr">
|
|
|
- <span class="review-kr-num">KR{{ i+1 }}</span>
|
|
|
- <span class="review-kr-text">{{ kr.title }}</span>
|
|
|
- <span class="review-kr-val">{{ kr.targetValue }}</span>
|
|
|
- <el-tag v-if="kr.assignedToName" size="small" effect="dark" type="warning" round>@{{ kr.assignedToName }}</el-tag>
|
|
|
- </div>
|
|
|
- <div class="review-actions">
|
|
|
- <el-button size="small" type="success" @click="doReview(o.id, true)" round>通过</el-button>
|
|
|
- <el-button size="small" type="danger" @click="doReview(o.id, false)" round>驳回</el-button>
|
|
|
- </div>
|
|
|
+
|
|
|
+ <!-- 进度更新(仅 EXECUTING 阶段)-->
|
|
|
+ <template v-if="period?.status === 'EXECUTING' && drawerKr.status !== 'COMPLETED'">
|
|
|
+ <div class="uppercase-label" style="margin-top: 20px;">更新进度</div>
|
|
|
+ <div style="font-size: 11px; color: var(--color-text-muted); margin: 4px 0 8px;">
|
|
|
+ 当前值:{{ drawerKr.currentValue }}
|
|
|
+ </div>
|
|
|
+ <el-input-number v-model="drawerKr._newValue" :min="0" :max="drawerKr.targetValue*2" style="width: 100%;"
|
|
|
+ :placeholder="'新值(当前 ' + drawerKr.currentValue + ')'" />
|
|
|
+ <div class="uppercase-label" style="margin-top: 16px;">状态</div>
|
|
|
+ <el-select v-model="drawerKr._newStatus" style="width: 100%; margin-top: 4px;">
|
|
|
+ <el-option label="进行中" value="IN_PROGRESS" />
|
|
|
+ <el-option label="阻塞" value="BLOCKED" />
|
|
|
+ <el-option label="已完成" value="COMPLETED" />
|
|
|
+ <el-option label="未开始" value="NOT_STARTED" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="primary" style="width: 100%; margin-top: 16px;"
|
|
|
+ :disabled="drawerKr._newValue == null" @click="submitDrawerProgress">保存进度</el-button>
|
|
|
+ <div v-if="drawerKr.status === 'BLOCKED'" style="margin-top: 12px; font-size: 11px; color: var(--color-warning);">
|
|
|
+ 阻塞项,请说明原因与所需支持。上级已收到提醒。
|
|
|
</div>
|
|
|
+ </template>
|
|
|
+ <div v-else-if="drawerKr.status === 'COMPLETED'" class="uppercase-label"
|
|
|
+ style="margin-top: 20px; color: var(--color-success);">
|
|
|
+ 该 KR 已完成
|
|
|
</div>
|
|
|
</div>
|
|
|
- </template>
|
|
|
+ </el-drawer>
|
|
|
|
|
|
<!-- Create OKR Dialog -->
|
|
|
<el-dialog v-model="showForm" :title="editingOkr ? '编辑 OKR' : '制定个人 OKR'" width="720px" top="4vh" :close-on-click-modal="false">
|
|
|
@@ -202,7 +184,7 @@
|
|
|
<div class="dkr-title">你的岗位指标参考({{ positionTemplate.name }})</div>
|
|
|
<div class="dkr-tags">
|
|
|
<el-tag v-for="(item, i) in parseKpiItems(positionTemplate.itemsJson)" :key="i"
|
|
|
- size="small" type="info" effect="plain" round>
|
|
|
+ size="small" type="info" effect="plain">
|
|
|
{{ item.name }}({{ item.maxScore }}分)
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
@@ -217,7 +199,7 @@
|
|
|
<span class="align-head-title">对齐上级 OKR</span>
|
|
|
<span class="align-head-hint">· 勾选你要承接的关键结果,将自动创建引用条目</span>
|
|
|
</div>
|
|
|
- <el-tag v-if="mandatoryKrCount" type="warning" effect="dark" round size="small">
|
|
|
+ <el-tag v-if="mandatoryKrCount" type="warning" effect="plain" size="small">
|
|
|
{{ mandatoryKrCount }} 项指派给我
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
@@ -244,7 +226,7 @@
|
|
|
</div>
|
|
|
<div class="align-kr-right">
|
|
|
<span class="align-kr-target">目标 {{ kr.targetValue }}</span>
|
|
|
- <el-tag v-if="kr.assignedTo === auth.user?.id" type="warning" effect="dark" size="small" round>
|
|
|
+ <el-tag v-if="kr.assignedTo === auth.user?.id" type="warning" effect="plain" size="small">
|
|
|
指派给我
|
|
|
</el-tag>
|
|
|
<span v-else-if="isKrAligned(kr.id)" class="align-check">已选</span>
|
|
|
@@ -307,7 +289,7 @@
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <el-button v-if="newOkr.krs.length < 5" @click="newOkr.krs.push({ title: '', targetValue: 100, weight: 0, assignedTo: null })" size="small" round style="margin-top:8px">
|
|
|
+ <el-button v-if="newOkr.krs.length < 5" @click="newOkr.krs.push({ title: '', targetValue: 100, weight: 0, assignedTo: null })" size="small" style="margin-top:8px">
|
|
|
新增 KR
|
|
|
</el-button>
|
|
|
</div>
|
|
|
@@ -318,7 +300,7 @@
|
|
|
|
|
|
<template #footer>
|
|
|
<el-button @click="showForm = false">取消</el-button>
|
|
|
- <el-button type="primary" size="large" @click="handleSubmit" :loading="submitting" round>
|
|
|
+ <el-button type="primary" size="large" @click="handleSubmit" :loading="submitting">
|
|
|
{{ editingOkr ? '保存修改' : '提交审核' }}
|
|
|
</el-button>
|
|
|
</template>
|
|
|
@@ -515,15 +497,6 @@ function initAlignment() {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-const stateIcon = computed(() => ({ DRAFT: '', OKR_ALIGN: '', EXECUTING: '', ASSESSING: '', ARCHIVED: '' }[period.value?.status] || ''))
|
|
|
-const stateBanner = computed(() => ({
|
|
|
- DRAFT: '本周期尚未发布。',
|
|
|
- OKR_ALIGN: 'OKR 对齐阶段 — 自上而下:上级先制定,下级对齐后制定。1O+3KR,不可偏离上级方向。',
|
|
|
- EXECUTING: '执行阶段 — OKR 已锁定。请每周更新 KR 进度,标记阻塞时说明原因。',
|
|
|
- ASSESSING: '考评阶段 — 前往"我的绩效"完成自评,基于实际交付结果打分。',
|
|
|
- ARCHIVED: '本周期已归档。'
|
|
|
-}[period.value?.status] || ''))
|
|
|
-
|
|
|
onMounted(async () => {
|
|
|
periods.value = await periodApi.list()
|
|
|
// Respect periodId from query param first, then default to first active
|
|
|
@@ -652,104 +625,86 @@ async function updateProgress(kr) {
|
|
|
ElMessage.success('进度已更新'); load()
|
|
|
}
|
|
|
|
|
|
+// v2 侧边抽屉:KR 详情 / 进度更新
|
|
|
+const drawerVisible = ref(false)
|
|
|
+const drawerKr = ref(null)
|
|
|
+
|
|
|
+function openKrDrawer(kr) {
|
|
|
+ // 复制一份并初始化 updateProgress 机制所需的临时字段
|
|
|
+ drawerKr.value = { ...kr, _newValue: kr.currentValue, _newStatus: kr.status }
|
|
|
+ drawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function submitDrawerProgress() {
|
|
|
+ if (!drawerKr.value) return
|
|
|
+ await updateProgress(drawerKr.value)
|
|
|
+ drawerVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// v2 工具栏状态显示
|
|
|
+const periodStatusLabel = computed(() => {
|
|
|
+ const map = { DRAFT: '草稿', OKR_ALIGN: '对齐中', EXECUTING: '执行中', ASSESSING: '考评中', ARCHIVED: '已归档' }
|
|
|
+ return map[period.value?.status] || '—'
|
|
|
+})
|
|
|
+const periodStatusDotClass = computed(() => {
|
|
|
+ const map = { EXECUTING: 'status-label__dot--active', OKR_ALIGN: 'status-label__dot--warn', ASSESSING: 'status-label__dot--warn', ARCHIVED: 'status-label__dot--info' }
|
|
|
+ return map[period.value?.status] || ''
|
|
|
+})
|
|
|
+const daysLeft = computed(() => {
|
|
|
+ if (!period.value?.endDate) return null
|
|
|
+ const diff = Math.ceil((new Date(period.value.endDate) - new Date()) / 86400000)
|
|
|
+ return diff > 0 ? diff : 0
|
|
|
+})
|
|
|
+function krStatusDotClass(kr) {
|
|
|
+ if (kr.status === 'BLOCKED') return 'status-dot--warn'
|
|
|
+ if (kr.status === 'COMPLETED') return 'status-dot--active'
|
|
|
+ return 'status-dot--info'
|
|
|
+}
|
|
|
+
|
|
|
async function doReview(id, approved) {
|
|
|
await okrApi.review(id, { approved, remark: '' })
|
|
|
ElMessage.success(approved ? '已通过' : '已驳回'); load()
|
|
|
}
|
|
|
|
|
|
function krPercent(kr) { if (!kr.targetValue || kr.targetValue === 0) return 0; return Math.min((kr.currentValue / kr.targetValue) * 100, 100) }
|
|
|
-function krColor(s) { return { COMPLETED: 'var(--color-success)', BLOCKED: 'var(--color-warning)', IN_PROGRESS: 'var(--color-accent)', NOT_STARTED: 'var(--color-text-muted)' }[s] || 'var(--color-text-muted)' }
|
|
|
function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进行中', COMPLETED: '已完成', BLOCKED: '阻塞' }[s] || s }
|
|
|
-function truncate(s, n) { if (!s) return ''; return s.length > n ? s.slice(0, n) + '...' : s }
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.page { width: 100%; }
|
|
|
-.page-hero { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
|
|
|
-.hero-info h2 { font-size: var(--font-size-2xl); font-weight: 700; color: var(--color-text); margin: 0; }
|
|
|
-.hero-info p { font-size: var(--font-size-base); color: var(--color-text-secondary); margin: 4px 0 0; }
|
|
|
-.empty-big { display: flex; align-items: center; justify-content: center; height: 300px; font-size: var(--font-size-lg); color: var(--color-text-muted); }
|
|
|
|
|
|
-.state-banner { padding: 14px 20px; border-radius: var(--radius-md); margin-bottom: 16px; display: flex; align-items: center; gap: 10px; font-size: var(--font-size-base); }
|
|
|
-.banner-OKR_ALIGN { background: var(--color-surface-tint-info); color: var(--color-medical); border: 1px solid var(--color-accent-soft); }
|
|
|
+/* v2 状态标签色点变体 */
|
|
|
+.status-label__dot--active { background: var(--color-success); }
|
|
|
+.status-label__dot--warn { background: var(--color-warning); }
|
|
|
+.status-label__dot--info { background: var(--color-text-muted); }
|
|
|
+.status-dot--active { background: var(--color-success); }
|
|
|
+.status-dot--warn { background: var(--color-warning); }
|
|
|
+.status-dot--info { background: var(--color-text-muted); }
|
|
|
+
|
|
|
+.okr-empty {
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ height: 240px; color: var(--color-text-muted); font-size: var(--font-size-base);
|
|
|
+}
|
|
|
+
|
|
|
+/* 状态提示横幅(v2,驳回/等待/锁定提示)*/
|
|
|
+.state-banner {
|
|
|
+ padding: 12px 16px; border-radius: var(--radius-sm); margin-bottom: 12px;
|
|
|
+ display: flex; align-items: center; gap: 10px; font-size: var(--font-size-sm);
|
|
|
+}
|
|
|
.banner-EXECUTING { background: var(--color-surface-tint-success); color: var(--color-success); border: 1px solid var(--color-border-soft); }
|
|
|
-.banner-ASSESSING { background: var(--color-surface-tint-warning); color: var(--color-warning); border: 1px solid var(--color-border-soft); }
|
|
|
-
|
|
|
-.rules-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 20px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
|
|
|
-.rules-title { font-weight: 700; color: var(--color-brand); font-size: var(--font-size-base); margin-bottom: 8px; }
|
|
|
-.rules-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
|
-.rule-item { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
|
|
|
-
|
|
|
-.position-ref { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border-soft); }
|
|
|
-.pr-title { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-brand); margin-bottom: 6px; }
|
|
|
-.pr-items { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
|
-.pr-hint { font-size: 11px; color: var(--color-text-muted); margin-top: 6px; }
|
|
|
-
|
|
|
-.section { margin-bottom: 36px; }
|
|
|
-.section-head { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--color-border-soft); }
|
|
|
-.section-badge { padding: 4px 12px; border-radius: var(--radius-sm); font-size: var(--font-size-sm); font-weight: 700; color: var(--color-brand); letter-spacing: 0; background: var(--color-surface-tint-brand); border-left: 2px solid var(--color-brand); }
|
|
|
-.section-badge.personal { color: var(--color-medical); background: var(--color-surface-tint-info); border-left-color: var(--color-accent); }
|
|
|
-.section-badge.superior { color: var(--color-brand); background: var(--color-surface-tint-brand); border-left-color: var(--color-brand); }
|
|
|
-.section-badge.review { color: var(--color-warning); background: var(--color-surface-tint-warning); border-left-color: var(--color-warning); }
|
|
|
-.section-title { font-size: var(--font-size-xl); font-weight: 700; color: var(--color-text); }
|
|
|
-.section-hint { font-size: var(--font-size-sm); color: var(--color-text-muted); }
|
|
|
-
|
|
|
-.hint-card { display: flex; align-items: flex-start; gap: 20px; background: var(--color-surface); border-radius: var(--radius-md); padding: 28px 32px; box-shadow: var(--shadow-panel); margin-bottom: 24px; }
|
|
|
-.hint-card.waiting { border: 1px solid var(--color-border-soft); background: var(--color-surface-tint-warning); }
|
|
|
-.hint-card.top-level { border: 1px solid var(--color-border-soft); background: var(--color-surface-tint-success); }
|
|
|
-.hint-icon { font-size: 36px; }
|
|
|
-.hint-title { font-size: var(--font-size-lg); font-weight: 700; color: var(--color-text); margin-bottom: 4px; }
|
|
|
-.hint-desc { font-size: var(--font-size-sm); color: var(--color-text-secondary); line-height: 1.6; }
|
|
|
-.action-row { margin-bottom: 24px; }
|
|
|
-
|
|
|
-.rule-hint { font-size: var(--font-size-xs); color: var(--color-warning); background: var(--color-surface-tint-warning); padding: 8px 14px; border-radius: var(--radius-sm); margin-bottom: 8px; }
|
|
|
-
|
|
|
-.okr-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
|
|
-.okr-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 20px 24px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
|
|
|
-.okr-card.superior-card { border-left: 4px solid var(--color-brand); }
|
|
|
-.okr-card.card-APPROVED { border-left: 4px solid var(--color-success); }
|
|
|
-.okr-card.card-SUBMITTED { border-left: 4px solid var(--color-warning); }
|
|
|
-.okr-card.card-REJECTED { border-left: 4px solid var(--color-danger); background: var(--color-surface-tint-danger); }
|
|
|
-.okr-card.review-card { border-left: 4px solid var(--color-warning); }
|
|
|
-.okr-card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
|
-.o-tag { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); font-size: var(--font-size-base); font-weight: 700; color: var(--color-white); }
|
|
|
-.o-tag.superior { background: var(--color-brand); }
|
|
|
-.o-tag.personal { background: var(--color-accent); }
|
|
|
-.o-title { font-size: 15px; font-weight: 600; color: var(--color-text); flex: 1; }
|
|
|
-
|
|
|
-.aligned-hint { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; font-size: var(--font-size-xs); color: var(--color-text-muted); flex-wrap: wrap; }
|
|
|
-
|
|
|
-.mini-kr { display: flex; align-items: center; gap: 8px; font-size: var(--font-size-sm); color: var(--color-text-secondary); padding: 4px 6px; border-radius: var(--radius-sm); }
|
|
|
-.mini-kr.mine { background: var(--color-surface-tint-warning); border: 1px solid var(--color-border-soft); }
|
|
|
-.mini-kr-num { font-size: 11px; font-weight: 700; color: var(--color-brand); width: 32px; }
|
|
|
-.mini-kr-text { flex: 1; }
|
|
|
-.mini-val { color: var(--color-text-muted); }
|
|
|
-
|
|
|
-
|
|
|
-.kr-display { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid var(--color-border-soft); }
|
|
|
-.kr-display:first-child { border-top: none; }
|
|
|
-.kr-display-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-accent); width: 36px; }
|
|
|
-.kr-display-text { flex: 1; font-size: var(--font-size-base); color: var(--color-text); }
|
|
|
-.kr-display-meta { display: flex; align-items: center; gap: 8px; }
|
|
|
-.kr-target { font-size: var(--font-size-sm); color: var(--color-text-muted); }
|
|
|
-
|
|
|
-.kr-progress-row { padding: 16px 0; border-top: 1px solid var(--color-border-soft); }
|
|
|
-.kr-progress-row:first-child { border-top: none; }
|
|
|
-.kr-progress-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
|
|
|
-.kr-progress-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-accent); width: 36px; }
|
|
|
-.kr-progress-text { flex: 1; font-size: var(--font-size-base); color: var(--color-text); }
|
|
|
-.kr-progress-val { font-size: var(--font-size-sm); color: var(--color-text-muted); }
|
|
|
-.kr-progress-ctrl { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
|
|
-.kr-status-tag { font-size: 11px; color: var(--color-text-secondary); width: 60px; }
|
|
|
-.blocked-hint { font-size: var(--font-size-xs); color: var(--color-warning); margin-top: 6px; }
|
|
|
-
|
|
|
-.review-kr { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 13px; }
|
|
|
-.review-kr-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-warning); width: 32px; }
|
|
|
-.review-kr-text { flex: 1; color: var(--color-text-secondary); }
|
|
|
-.review-kr-val { color: var(--color-text-muted); }
|
|
|
-.review-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-border-soft); }
|
|
|
-
|
|
|
-.rejected-hint { padding: 14px; background: var(--color-surface-tint-danger); border-radius: var(--radius-md); font-size: var(--font-size-sm); color: var(--color-danger); margin-top: 12px; }
|
|
|
+.banner-REJECTED { background: var(--color-surface-tint-danger); color: var(--color-danger); border: 1px solid var(--color-border-soft); }
|
|
|
+.banner-waiting { background: var(--color-surface-tint-warning); color: var(--color-warning); border: 1px solid var(--color-border-soft); }
|
|
|
+.banner-top-level { background: var(--color-surface-tint-success); color: var(--color-success); border: 1px solid var(--color-border-soft); }
|
|
|
+
|
|
|
+/* KR 来源标签(承接上级/指派)*/
|
|
|
+.kr-source-tag {
|
|
|
+ display: inline-block; margin-left: 6px; padding: 1px 6px;
|
|
|
+ background: var(--color-surface-tint-info); color: var(--color-medical);
|
|
|
+ border-radius: 3px; font-size: 10px; font-weight: 500;
|
|
|
+}
|
|
|
+.kr-source-tag--mine {
|
|
|
+ background: var(--color-surface-tint-warning); color: var(--color-warning);
|
|
|
+}
|
|
|
|
|
|
/* Dialog */
|
|
|
.dialog-rules { background: var(--color-surface-tint-info); border-radius: var(--radius-md); padding: 14px 18px; margin-bottom: 16px; }
|