|
|
@@ -4,151 +4,303 @@
|
|
|
:empty="!workspace.hasPeriod" empty-message="暂无活跃考核周期"
|
|
|
@retry="workspace.refreshOverview()">
|
|
|
<template v-if="workspace.hasPeriod">
|
|
|
- <!-- Stat cards row -->
|
|
|
- <div class="grid-3">
|
|
|
- <router-link v-if="firstTodoTarget" :to="firstTodoTarget" class="stat-card workspace-todo-metric">
|
|
|
- <div class="stat-card__top stat-card__top--accent"></div>
|
|
|
- <div class="stat-card__num">{{ workspace.todos.length }}</div>
|
|
|
- <div class="stat-card__label">待办事项</div>
|
|
|
- </router-link>
|
|
|
- <div v-else class="stat-card workspace-todo-metric is-disabled">
|
|
|
- <div class="stat-card__top stat-card__top--accent"></div>
|
|
|
- <div class="stat-card__num">0</div>
|
|
|
- <div class="stat-card__label">待办事项</div>
|
|
|
+ <header class="workspace-heading">
|
|
|
+ <div>
|
|
|
+ <span class="workspace-heading__eyebrow">CURRENT PERIOD</span>
|
|
|
+ <h1>{{ periodTitle }}</h1>
|
|
|
</div>
|
|
|
- <router-link :to="teamOkrTarget" class="stat-card workspace-team-link" v-if="workspace.teamSummary">
|
|
|
- <div class="stat-card__top stat-card__top--indigo"></div>
|
|
|
- <div class="stat-card__num">{{ workspace.teamSummary.totalMembers }}</div>
|
|
|
- <div class="stat-card__label">团队成员 · 已提交 {{ workspace.teamSummary.okrSubmitted }}</div>
|
|
|
+ <span class="workspace-heading__phase">{{ phaseLabel }}</span>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="workspace-primary">
|
|
|
+ <router-link v-if="firstTodo" :to="firstTodoTarget" class="workspace-action">
|
|
|
+ <span class="workspace-action__eyebrow">YOUR NEXT ACTION</span>
|
|
|
+ <h2 class="workspace-action__title">{{ firstTodo.label }}</h2>
|
|
|
+ <p>{{ firstTodoDescription }}</p>
|
|
|
+ <span class="workspace-action__cta">
|
|
|
+ 立即处理
|
|
|
+ <span aria-hidden="true">→</span>
|
|
|
+ </span>
|
|
|
</router-link>
|
|
|
- <div class="stat-card">
|
|
|
- <div class="stat-card__top" :class="workspace.risks.length ? 'stat-card__top--orange' : 'stat-card__top--green'"></div>
|
|
|
- <div class="stat-card__num">{{ workspace.risks.length }}</div>
|
|
|
- <div class="stat-card__label">风险项</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <section v-else class="workspace-action workspace-action--empty">
|
|
|
+ <span class="workspace-action__eyebrow">YOUR NEXT ACTION</span>
|
|
|
+ <h2 class="workspace-action__title">当前没有待办</h2>
|
|
|
+ <p>本周期的个人行动已处理完毕,可以关注后续节点与团队进展。</p>
|
|
|
+ </section>
|
|
|
|
|
|
- <!-- Todo + Risks row -->
|
|
|
- <div class="grid-2 mt-md">
|
|
|
- <!-- My todos -->
|
|
|
- <div class="card">
|
|
|
- <div class="s-head mb-sm">
|
|
|
- <div class="s-head__left">
|
|
|
- <h2 style="font-size:var(--font-size-xl);margin:0">我的待办</h2>
|
|
|
- </div>
|
|
|
+ <section class="workspace-context">
|
|
|
+ <div class="workspace-context__heading">
|
|
|
+ <span>周期上下文</span>
|
|
|
+ <span>{{ workspace.currentPeriod?.startDate }} — {{ workspace.currentPeriod?.endDate }}</span>
|
|
|
</div>
|
|
|
- <div v-if="!workspace.todos.length" class="text-muted text-sm" style="padding:12px 0">暂无待办</div>
|
|
|
- <router-link v-for="todo in workspace.todos" :key="todo.type"
|
|
|
- :to="todo.targetUrl" class="workspace-todo">
|
|
|
- <span class="workspace-todo__label">{{ todo.label }}</span>
|
|
|
- <span v-if="todo.count" class="tag tag-warn">{{ todo.count }}</span>
|
|
|
- </router-link>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- Risks -->
|
|
|
- <div class="card">
|
|
|
- <div class="s-head mb-sm">
|
|
|
- <div class="s-head__left">
|
|
|
- <h2 style="font-size:var(--font-size-xl);margin:0">风险预警</h2>
|
|
|
+ <div class="workspace-context__group">
|
|
|
+ <h2>截止节点</h2>
|
|
|
+ <div v-if="workspace.deadlines.length" class="workspace-context__list">
|
|
|
+ <div v-for="deadline in workspace.deadlines" :key="`${deadline.label}-${deadline.date}`"
|
|
|
+ class="workspace-context__row">
|
|
|
+ <span>{{ deadline.label }}</span>
|
|
|
+ <time :class="{ 'is-urgent': deadline.type === 'URGENT' }">{{ deadline.date }}</time>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <p v-else class="workspace-context__quiet">当前没有临近截止节点</p>
|
|
|
</div>
|
|
|
- <div v-if="!workspace.risks.length" class="text-muted text-sm" style="padding:12px 0">暂无风险</div>
|
|
|
- <div v-for="r in workspace.risks" :key="r.label" class="workspace-risk">
|
|
|
- <span class="tag tag-warn">{{ r.label }}</span>
|
|
|
- <span class="workspace-risk__user" v-if="r.userName">{{ r.userName }}</span>
|
|
|
- </div>
|
|
|
- <!-- Deadlines -->
|
|
|
- <div v-if="workspace.deadlines.length" class="mt-sm">
|
|
|
- <div class="text-xs text-muted mb-sm">近期节点</div>
|
|
|
- <div v-for="d in workspace.deadlines" :key="d.label" class="workspace-deadline">
|
|
|
- <span>{{ d.label }}</span>
|
|
|
- <span :class="{ 'text-danger': d.type === 'URGENT' }" class="mono">{{ d.date }}</span>
|
|
|
+
|
|
|
+ <div class="workspace-context__group">
|
|
|
+ <h2>风险状态</h2>
|
|
|
+ <div v-if="workspace.risks.length" class="workspace-context__list">
|
|
|
+ <div v-for="risk in workspace.risks" :key="`${risk.type}-${risk.label}-${risk.userName || ''}`"
|
|
|
+ class="workspace-context__row workspace-context__risk">
|
|
|
+ <span>{{ risk.label }}</span>
|
|
|
+ <span v-if="risk.userName">{{ risk.userName }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <p v-else class="workspace-health"><span aria-hidden="true"></span>暂无风险,周期运行正常</p>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+
|
|
|
+ <div v-if="workspace.teamSummary" class="workspace-context__team">
|
|
|
+ 团队已提交 OKR
|
|
|
+ <strong>{{ workspace.teamSummary.okrSubmitted }} / {{ workspace.teamSummary.totalMembers }}</strong>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Team summary table -->
|
|
|
- <div class="card mt-md" v-if="workspace.teamSummary">
|
|
|
- <div class="s-head mb-sm">
|
|
|
- <div class="s-head__left">
|
|
|
- <h2 style="font-size:var(--font-size-xl);margin:0">团队概览</h2>
|
|
|
+ <section v-if="workspace.teamSummary" class="workspace-team">
|
|
|
+ <header class="workspace-team__heading">
|
|
|
+ <h2>团队进展</h2>
|
|
|
+ <router-link :to="teamOkrTarget" class="workspace-team-link">查看团队 OKR →</router-link>
|
|
|
+ </header>
|
|
|
+ <div class="workspace-pulse">
|
|
|
+ <div class="workspace-pulse__item">
|
|
|
+ <strong>{{ workspace.teamSummary.totalMembers }}</strong>
|
|
|
+ <span>团队成员</span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="workspace-summary-grid">
|
|
|
- <router-link :to="teamOkrTarget" class="workspace-summary__item workspace-team-link">
|
|
|
- <span class="workspace-summary__value">{{ workspace.teamSummary.totalMembers }}</span>
|
|
|
- <span class="workspace-summary__label">团队成员</span>
|
|
|
- </router-link>
|
|
|
- <div class="workspace-summary__item">
|
|
|
- <span class="workspace-summary__value">{{ workspace.teamSummary.okrSubmitted }}</span>
|
|
|
- <span class="workspace-summary__label">已提交 OKR</span>
|
|
|
+ <div class="workspace-pulse__item">
|
|
|
+ <strong>{{ workspace.teamSummary.okrSubmitted }}</strong>
|
|
|
+ <span>已提交 OKR</span>
|
|
|
</div>
|
|
|
- <div class="workspace-summary__item">
|
|
|
- <span class="workspace-summary__value">{{ workspace.teamSummary.selfScoreSubmitted || 0 }}</span>
|
|
|
- <span class="workspace-summary__label">已自评</span>
|
|
|
+ <div class="workspace-pulse__item">
|
|
|
+ <strong>{{ workspace.teamSummary.selfScoreSubmitted || 0 }}</strong>
|
|
|
+ <span>已完成自评</span>
|
|
|
</div>
|
|
|
- <div class="workspace-summary__item">
|
|
|
- <span class="workspace-summary__value">{{ workspace.teamSummary.finalScoreCompleted || 0 }}</span>
|
|
|
- <span class="workspace-summary__label">已完成终评</span>
|
|
|
+ <div class="workspace-pulse__item">
|
|
|
+ <strong>{{ workspace.teamSummary.finalScoreCompleted || 0 }}</strong>
|
|
|
+ <span>已完成终评</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </section>
|
|
|
</template>
|
|
|
</AsyncState>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, ref, watch } from 'vue'
|
|
|
+import { computed } from 'vue'
|
|
|
import { useWorkspaceStore } from '@/stores/workspace'
|
|
|
import AsyncState from '@/components/common/AsyncState.vue'
|
|
|
|
|
|
const workspace = useWorkspaceStore()
|
|
|
|
|
|
-const selectedPeriodId = ref(null)
|
|
|
-
|
|
|
+const periodTitle = computed(() => workspace.currentPeriod?.name || '当前周期')
|
|
|
const phaseLabel = computed(() => {
|
|
|
const map = { DRAFT: '草稿', OKR_ALIGN: 'OKR 对齐', EXECUTING: '执行中', ASSESSING: '考评中', ARCHIVED: '已归档' }
|
|
|
- return map[workspace.currentPhase] || workspace.currentPhase || ''
|
|
|
+ return map[workspace.currentPhase] || workspace.currentPhase || '进行中'
|
|
|
+})
|
|
|
+const firstTodo = computed(() => workspace.todos[0] || null)
|
|
|
+const firstTodoTarget = computed(() => firstTodo.value?.targetUrl || '')
|
|
|
+const firstTodoDescription = computed(() => {
|
|
|
+ return firstTodo.value?.count
|
|
|
+ ? `当前有 ${firstTodo.value.count} 项需要处理,请优先完成这一项。`
|
|
|
+ : '请优先完成这一项,推动本周期工作继续向前。'
|
|
|
})
|
|
|
-const firstTodoTarget = computed(() => workspace.todos[0]?.targetUrl || '')
|
|
|
const teamOkrTarget = computed(() => ({
|
|
|
path: '/okr/team',
|
|
|
query: workspace.periodId ? { periodId: workspace.periodId } : {}
|
|
|
}))
|
|
|
-
|
|
|
-watch(() => workspace.currentPeriod, (p) => {
|
|
|
- selectedPeriodId.value = p?.id || null
|
|
|
-}, { immediate: true })
|
|
|
-
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.workspace-view { max-width: 1120px; margin: 0 auto; }
|
|
|
-.workspace-todo {
|
|
|
- display: flex; align-items: center; justify-content: space-between;
|
|
|
- padding: 10px 0; border-bottom: 1px solid var(--color-border-soft);
|
|
|
- cursor: pointer; text-decoration: none; color: var(--color-text);
|
|
|
- transition: color .15s;
|
|
|
-}
|
|
|
-.workspace-todo:hover { color: var(--color-dark); }
|
|
|
-.workspace-todo:last-child { border-bottom: none; }
|
|
|
-.workspace-todo__label { font-size: var(--font-size-sm); font-weight: 500; }
|
|
|
-.workspace-todo-metric, .workspace-team-link { display: block; text-decoration: none; color: inherit; }
|
|
|
-.workspace-todo-metric:not(.is-disabled), .workspace-team-link { cursor: pointer; }
|
|
|
-.workspace-todo-metric:not(.is-disabled):hover, .workspace-team-link:hover { border-color: var(--color-accent); }
|
|
|
-.workspace-todo-metric.is-disabled { cursor: default; }
|
|
|
-.workspace-risk { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--color-border-soft); }
|
|
|
-.workspace-risk:last-child { border-bottom: none; }
|
|
|
-.workspace-risk__user { font-size: var(--font-size-xs); color: var(--color-text-muted); }
|
|
|
-.workspace-deadline { display: flex; justify-content: space-between; padding: 6px 0; font-size: var(--font-size-sm); border-bottom: 1px solid var(--color-border-soft); }
|
|
|
-.workspace-deadline:last-child { border-bottom: none; }
|
|
|
-.workspace-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--spacing-sm); padding: var(--spacing-md) 0; }
|
|
|
-.workspace-summary__item { min-height: 92px; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: var(--spacing-xs); text-align: center; border-right: 1px solid var(--color-border-soft); }
|
|
|
-.workspace-summary__item:last-child { border-right: none; }
|
|
|
-.workspace-summary__value { display: block; color: var(--color-text); font-size: var(--font-size-2xl); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
|
-.workspace-summary__label { display: block; color: var(--color-text-muted); font-size: var(--font-size-sm); line-height: 1.4; }
|
|
|
-@media (max-width: 640px) { .workspace-summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.workspace-summary__item:nth-child(2) { border-right: none; }.workspace-summary__item:nth-child(-n+2) { border-bottom: 1px solid var(--color-border-soft); } }
|
|
|
-@media (max-width: 768px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }
|
|
|
+.workspace-view {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 1120px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-heading {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 24px;
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-heading__eyebrow,
|
|
|
+.workspace-action__eyebrow {
|
|
|
+ display: block;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: .14em;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-heading__eyebrow { color: var(--color-text-secondary); }
|
|
|
+.workspace-heading h1 { margin: 6px 0 0; font-size: 28px; line-height: 1.2; }
|
|
|
+.workspace-heading__phase {
|
|
|
+ padding: 6px 12px;
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ border-radius: 999px;
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-primary {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, .65fr);
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ background: var(--color-surface);
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-action {
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 368px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 46px;
|
|
|
+ background: var(--color-dark);
|
|
|
+ color: var(--color-white);
|
|
|
+ text-decoration: none;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-action:hover { color: var(--color-white); }
|
|
|
+.workspace-action__eyebrow { color: rgba(255, 255, 255, .7); }
|
|
|
+.workspace-action__title {
|
|
|
+ max-width: 560px;
|
|
|
+ margin: 22px 0 12px;
|
|
|
+ color: var(--color-white);
|
|
|
+ font-size: clamp(32px, 4vw, 48px);
|
|
|
+ line-height: 1.08;
|
|
|
+}
|
|
|
+.workspace-action p {
|
|
|
+ max-width: 520px;
|
|
|
+ margin: 0;
|
|
|
+ color: rgba(255, 255, 255, .78);
|
|
|
+ font-size: 15px;
|
|
|
+ line-height: 1.75;
|
|
|
+}
|
|
|
+.workspace-action__cta {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ margin-top: auto;
|
|
|
+ padding: 11px 16px;
|
|
|
+ background: var(--color-accent);
|
|
|
+ color: var(--color-dark-deeper);
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.workspace-action--empty { cursor: default; }
|
|
|
+
|
|
|
+.workspace-context {
|
|
|
+ min-width: 0;
|
|
|
+ padding: 30px;
|
|
|
+ border-left: 1px solid var(--color-border);
|
|
|
+}
|
|
|
+.workspace-context__heading {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ padding-bottom: 18px;
|
|
|
+ border-bottom: 1px solid var(--color-border);
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.workspace-context__heading span:first-child { font-weight: 700; }
|
|
|
+.workspace-context__heading span:last-child { color: var(--color-text-secondary); text-align: right; }
|
|
|
+.workspace-context__group { padding: 20px 0; border-bottom: 1px solid var(--color-border-soft); }
|
|
|
+.workspace-context__group h2 {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: .08em;
|
|
|
+}
|
|
|
+.workspace-context__list { display: grid; gap: 9px; }
|
|
|
+.workspace-context__row {
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ color: var(--color-text);
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.workspace-context__row > span:first-child { min-width: 0; overflow-wrap: anywhere; }
|
|
|
+.workspace-context__row time,
|
|
|
+.workspace-context__risk span:last-child { color: var(--color-text-secondary); white-space: nowrap; }
|
|
|
+.workspace-context__row time.is-urgent { color: var(--color-danger); font-weight: 600; }
|
|
|
+.workspace-context__quiet,
|
|
|
+.workspace-health { margin: 0; color: var(--color-text-secondary); font-size: 13px; }
|
|
|
+.workspace-health { display: flex; align-items: center; gap: 8px; }
|
|
|
+.workspace-health span { width: 7px; height: 7px; border-radius: 50%; background: var(--color-success); }
|
|
|
+.workspace-context__team {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ padding-top: 20px;
|
|
|
+ color: var(--color-text-secondary);
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.workspace-context__team strong { color: var(--color-text); }
|
|
|
+
|
|
|
+.workspace-team { margin-top: 22px; }
|
|
|
+.workspace-team__heading {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.workspace-team__heading h2 { margin: 0; font-size: 18px; }
|
|
|
+.workspace-team-link { color: var(--color-text-secondary); font-size: 13px; text-decoration: none; }
|
|
|
+.workspace-team-link:hover { color: var(--color-dark); }
|
|
|
+.workspace-pulse {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
+ background: var(--color-surface);
|
|
|
+}
|
|
|
+.workspace-pulse__item {
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 7px;
|
|
|
+ padding: 22px 26px;
|
|
|
+ border-right: 1px solid var(--color-border);
|
|
|
+ color: inherit;
|
|
|
+ text-decoration: none;
|
|
|
+}
|
|
|
+.workspace-pulse__item:last-child { border-right: 0; }
|
|
|
+.workspace-pulse__item strong { font-size: 24px; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
|
+.workspace-pulse__item span { color: var(--color-text-secondary); font-size: 12px; }
|
|
|
+
|
|
|
+@media (max-width: 820px) {
|
|
|
+ .workspace-primary { grid-template-columns: 1fr; }
|
|
|
+ .workspace-action { min-height: 320px; }
|
|
|
+ .workspace-context { border-top: 1px solid var(--color-border); border-left: 0; }
|
|
|
+ .workspace-pulse { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
+ .workspace-pulse__item:nth-child(2) { border-right: 0; }
|
|
|
+ .workspace-pulse__item:nth-child(-n + 2) { border-bottom: 1px solid var(--color-border); }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 640px) {
|
|
|
+ .workspace-heading { align-items: flex-start; margin-bottom: 18px; }
|
|
|
+ .workspace-heading h1 { font-size: 23px; overflow-wrap: anywhere; }
|
|
|
+ .workspace-action { min-height: 300px; padding: 30px 24px; }
|
|
|
+ .workspace-action__title { margin-top: 18px; font-size: 32px; overflow-wrap: anywhere; }
|
|
|
+ .workspace-context { padding: 24px; }
|
|
|
+ .workspace-context__heading { flex-direction: column; gap: 5px; }
|
|
|
+ .workspace-context__heading span:last-child { text-align: left; }
|
|
|
+ .workspace-context__row { gap: 12px; }
|
|
|
+ .workspace-context__risk { align-items: flex-start; }
|
|
|
+ .workspace-pulse__item { padding: 20px; }
|
|
|
+}
|
|
|
</style>
|