Browse Source

feat(frontend): 工作台首页(待办/团队/风险/截止时间)

wangkangyjy 1 week ago
parent
commit
c8fd59749a
1 changed files with 101 additions and 11 deletions
  1. 101 11
      frontend/src/views/workspace/WorkspaceView.vue

+ 101 - 11
frontend/src/views/workspace/WorkspaceView.vue

@@ -1,30 +1,120 @@
 <template>
   <div class="workspace-view">
-    <div class="data-toolbar">
-      <span class="data-toolbar__title">工作台</span>
-      <span class="data-toolbar__sep">|</span>
-      <span class="data-toolbar__meta" v-if="workspace.currentPeriod">
-        {{ workspace.currentPeriod.name }} · {{ phaseLabel }}
-      </span>
-    </div>
-    <div class="workspace-grid">
-      <p>工作台页面(将在任务9完善)</p>
-    </div>
+    <AsyncState :loading="workspace.loading" :error="!!workspace.error" :error-message="workspace.error"
+                :empty="!workspace.hasPeriod" empty-message="暂无活跃考核周期"
+                @retry="workspace.refreshOverview()">
+      <div class="data-toolbar">
+        <span class="data-toolbar__title">工作台</span>
+        <span class="data-toolbar__sep">|</span>
+        <span class="data-toolbar__meta" v-if="workspace.currentPeriod">
+          {{ workspace.currentPeriod.name }} · {{ phaseLabel }}
+        </span>
+        <span class="data-toolbar__spacer"></span>
+        <el-select v-model="selectedPeriodId" size="small" placeholder="切换周期" style="width:180px"
+                   @change="workspace.selectPeriod($event)" v-if="workspace.availablePeriods.length">
+          <el-option v-for="p in workspace.availablePeriods" :key="p.id" :label="p.name" :value="p.id" />
+        </el-select>
+      </div>
+
+      <div class="workspace-grid" v-if="workspace.hasPeriod">
+        <!-- 我的待办 -->
+        <section class="workspace-card">
+          <div class="uppercase-label">我的待办</div>
+          <div v-if="!workspace.todos.length" class="workspace-card__empty">暂无待办</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>
+            <el-tag v-if="todo.count" size="small" type="warning">{{ todo.count }}</el-tag>
+          </router-link>
+        </section>
+
+        <!-- 团队汇总 -->
+        <section class="workspace-card" v-if="workspace.teamSummary">
+          <div class="uppercase-label">团队汇总</div>
+          <div class="workspace-summary">
+            <div class="workspace-summary__item">
+              <span class="workspace-summary__num">{{ workspace.teamSummary.totalMembers }}</span>
+              <span class="workspace-summary__desc">团队成员</span>
+            </div>
+            <div class="workspace-summary__item">
+              <span class="workspace-summary__num">{{ workspace.teamSummary.okrSubmitted }}</span>
+              <span class="workspace-summary__desc">已提交 OKR</span>
+            </div>
+          </div>
+        </section>
+
+        <!-- 风险 -->
+        <section class="workspace-card" v-if="workspace.risks.length">
+          <div class="uppercase-label">风险项</div>
+          <div v-for="r in workspace.risks" :key="r.label" class="workspace-risk">
+            <StatusLabel tone="warning" :label="r.label" />
+            <span class="workspace-risk__user" v-if="r.userName">{{ r.userName }}</span>
+          </div>
+        </section>
+
+        <!-- 截止时间 -->
+        <section class="workspace-card" v-if="workspace.deadlines.length">
+          <div class="uppercase-label">近期节点</div>
+          <div v-for="d in workspace.deadlines" :key="d.label" class="workspace-deadline">
+            <span>{{ d.label }}</span>
+            <span :class="{ 'workspace-deadline--urgent': d.type === 'URGENT' }">{{ d.date }}</span>
+          </div>
+        </section>
+      </div>
+    </AsyncState>
   </div>
 </template>
 
 <script setup>
-import { computed, onMounted } from 'vue'
+import { computed, onMounted, ref, watch } from 'vue'
 import { useWorkspaceStore } from '@/stores/workspace'
+import AsyncState from '@/components/common/AsyncState.vue'
+import StatusLabel from '@/components/common/StatusLabel.vue'
 
 const workspace = useWorkspaceStore()
 
+const selectedPeriodId = ref(null)
+
 const phaseLabel = computed(() => {
   const map = { DRAFT: '草稿', OKR_ALIGN: 'OKR 对齐', EXECUTING: '执行中', ASSESSING: '考评中', ARCHIVED: '已归档' }
   return map[workspace.currentPhase] || workspace.currentPhase || ''
 })
 
+watch(() => workspace.currentPeriod, (p) => {
+  selectedPeriodId.value = p?.id || null
+}, { immediate: true })
+
 onMounted(() => {
   workspace.refreshOverview()
 })
 </script>
+
+<style scoped>
+.workspace-view { max-width: 960px; margin: 0 auto; }
+.workspace-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-lg, 16px); margin-top: var(--spacing-lg, 16px); }
+.workspace-card {
+  background: var(--workspace-surface, #fff);
+  border: 1px solid var(--color-border-soft, #edf2f6);
+  border-radius: var(--radius-md, 8px);
+  padding: var(--spacing-lg, 16px);
+}
+.workspace-card__empty { color: var(--color-text-muted); font-size: 13px; padding: 12px 0; }
+.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 var(--motion-fast, 160ms ease);
+}
+.workspace-todo:hover { color: var(--color-brand); }
+.workspace-todo:last-child { border-bottom: none; }
+.workspace-todo__label { font-size: 13px; font-weight: 500; }
+.workspace-summary { display: flex; gap: 32px; padding: 12px 0; }
+.workspace-summary__item { text-align: center; }
+.workspace-summary__num { font-size: 24px; font-weight: 700; color: var(--color-text); display: block; }
+.workspace-summary__desc { font-size: 11px; color: var(--color-text-muted); }
+.workspace-risk { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--color-border-soft); }
+.workspace-risk__user { font-size: 11px; color: var(--color-text-muted); }
+.workspace-deadline { display: flex; justify-content: space-between; padding: 8px 0; font-size: 13px; border-bottom: 1px solid var(--color-border-soft); }
+.workspace-deadline--urgent { color: var(--color-danger); font-weight: 600; }
+@media (max-width: 768px) { .workspace-grid { grid-template-columns: 1fr; } }
+</style>