Просмотр исходного кода

✨ feat(home): 重构 OKR 首页看板并优化信息可读性

Zimo 3 недель назад
Родитель
Сommit
e6308ee407
1 измененных файлов с 1334 добавлено и 362 удалено
  1. 1334 362
      src/views/Home/Index.vue

+ 1334 - 362
src/views/Home/Index.vue

@@ -1,384 +1,1356 @@
 <script lang="ts" setup>
-  import type { EChartsOption } from 'echarts'
+  import { computed, ref } from 'vue'
 
-  import { set } from 'lodash-es'
-  import { useRouter } from 'vue-router'
-
-  import { formatTime } from '@/utils'
   import { useUserStore } from '@/store/modules/user'
 
-  // import { useWatermark } from '@/hooks/web/useWatermark'
-  import type { Notice, Project, Shortcut, WorkplaceTotal } from './types'
+  defineOptions({ name: 'Index' })
 
-  import { barOptions, pieOptions } from './echarts-data'
+  type ProjectStatus = '正常' | '滞后' | '风险' | '已完成'
 
-  defineOptions({ name: 'Index' })
+  interface ProjectItem {
+    name: string
+    owner: string
+    progress: number
+    deadline: string
+    status: ProjectStatus
+    description: string
+  }
 
-  const { t } = useI18n()
-  const router = useRouter()
   const userStore = useUserStore()
-  // const { setWatermark } = useWatermark()
-  const loading = ref(true)
-  const avatar = userStore.getUser.avatar
-  const username = userStore.getUser.nickname
-  const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
-  // 获取统计数
-  let totalSate = reactive<WorkplaceTotal>({ project: 0, access: 0, todo: 0 })
-
-  const getCount = async () => {
-    const data = { project: 40, access: 2340, todo: 10 }
-    totalSate = Object.assign(totalSate, data)
-  }
-
-  // 获取项目数
-  let projects = reactive<Project[]>([])
-  const getProject = async () => {
-    const data = [
-      {
-        name: 'ruoyi-vue-pro',
-        icon: 'simple-icons:springboot',
-        message: 'github.com/YunaiV/ruoyi-vue-pro',
-        personal: 'Spring Boot 单体架构',
-        time: new Date('2025-01-02'),
-        color: '#6DB33F',
-      },
-      {
-        name: 'yudao-ui-admin-vue3',
-        icon: 'ep:element-plus',
-        message: 'github.com/yudaocode/yudao-ui-admin-vue3',
-        personal: 'Vue3 + element-plus 管理后台',
-        time: new Date('2025-02-03'),
-        color: '#409EFF',
-      },
-      {
-        name: 'yudao-ui-mall-uniapp',
-        icon: 'icon-park-outline:mall-bag',
-        message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
-        personal: 'Vue3 + uniapp 商城手机端',
-        time: new Date('2025-03-04'),
-        color: '#ff4d4f',
-      },
-      {
-        name: 'yudao-cloud',
-        icon: 'material-symbols:cloud-outline',
-        message: 'github.com/YunaiV/yudao-cloud',
-        personal: 'Spring Cloud 微服务架构',
-        time: new Date('2025-04-05'),
-        color: '#1890ff',
-      },
-      {
-        name: 'yudao-ui-admin-vben',
-        icon: 'devicon:antdesign',
-        message: 'github.com/yudaocode/yudao-ui-admin-vben',
-        personal: 'Vue3 + vben5(antd) 管理后台',
-        time: new Date('2025-05-06'),
-        color: '#e18525',
-      },
-      {
-        name: 'yudao-ui-admin-uniapp',
-        icon: 'ant-design:mobile',
-        message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
-        personal: 'Vue3 + uniapp 管理手机端',
-        time: new Date('2025-06-01'),
-        color: '#2979ff',
-      },
-    ]
-    projects = Object.assign(projects, data)
-  }
-
-  // 获取通知公告
-  let notice = reactive<Notice[]>([])
-  const getNotice = async () => {
-    const data = [
-      {
-        title: '系统支持 JDK 8/17/21,Vue 2/3',
-        type: '技术兼容性',
-        keys: ['JDK', 'Vue'],
-        date: new Date(),
-      },
-      {
-        title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
-        type: '架构灵活性',
-        keys: ['Boot', 'Cloud'],
-        date: new Date(),
-      },
-      {
-        title: '全部开源,个人与企业可 100% 直接使用,无需授权',
-        type: '开源免授权',
-        keys: ['无需授权'],
-        date: new Date(),
-      },
-      {
-        title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
-        type: '广泛企业认可',
-        keys: ['最广泛', '10w+'],
-        date: new Date(),
-      },
-    ]
-    notice = Object.assign(notice, data)
-  }
-
-  // 获取快捷入口
-  let shortcut = reactive<Shortcut[]>([])
-
-  const getShortcut = async () => {
-    const data = [
-      { name: '首页', icon: 'ion:home-outline', url: '/', color: '#1fdaca' },
-      { name: '商城中心', icon: 'ep:shop', url: '/mall/home', color: '#ff6b6b' },
-      { name: 'AI 大模型', icon: 'tabler:ai', url: '/ai/chat', color: '#7c3aed' },
-      { name: 'ERP 系统', icon: 'simple-icons:erpnext', url: '/erp/home', color: '#3fb27f' },
-      { name: 'CRM 系统', icon: 'simple-icons:civicrm', url: '/crm/backlog', color: '#4daf1bc9' },
-      { name: 'IoT 物联网', icon: 'fa-solid:hdd', url: '/iot/home', color: '#1a73e8' },
-    ]
-    shortcut = Object.assign(shortcut, data)
-  }
-
-  // 用户来源
-  const getUserAccessSource = async () => {
-    const data = [
-      { value: 335, name: 'analysis.directAccess' },
-      { value: 310, name: 'analysis.mailMarketing' },
-      { value: 234, name: 'analysis.allianceAdvertising' },
-      { value: 135, name: 'analysis.videoAdvertising' },
-      { value: 1548, name: 'analysis.searchEngines' },
-    ]
-    set(
-      pieOptionsData,
-      'legend.data',
-      data.map(v => t(v.name))
-    )
-    pieOptionsData!.series![0].data = data.map(v => {
-      return { name: t(v.name), value: v.value }
-    })
-  }
-  const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
-
-  // 周活跃量
-  const getWeeklyUserActivity = async () => {
-    const data = [
-      { value: 13253, name: 'analysis.monday' },
-      { value: 34235, name: 'analysis.tuesday' },
-      { value: 26321, name: 'analysis.wednesday' },
-      { value: 12340, name: 'analysis.thursday' },
-      { value: 24643, name: 'analysis.friday' },
-      { value: 1322, name: 'analysis.saturday' },
-      { value: 1324, name: 'analysis.sunday' },
-    ]
-    set(
-      barOptionsData,
-      'xAxis.data',
-      data.map(v => t(v.name))
-    )
-    set(barOptionsData, 'series', [
-      { name: t('analysis.activeQuantity'), data: data.map(v => v.value), type: 'bar' },
-    ])
-  }
-
-  const getAllApi = async () => {
-    await Promise.all([
-      getCount(),
-      getProject(),
-      getNotice(),
-      getShortcut(),
-      getUserAccessSource(),
-      getWeeklyUserActivity(),
-    ])
-    loading.value = false
-  }
-
-  const handleProjectClick = (message: string) => {
-    window.open(`https://${message}`, '_blank')
-  }
-
-  const handleShortcutClick = (url: string) => {
-    router.push(url)
-  }
-
-  getAllApi()
+  const username = computed(() => userStore.getUser.nickname || '管理者')
+  const activeFilter = ref('全部项目')
+  const filters = ['全部项目', '我的项目', '风险项目']
+
+  const metrics = [
+    {
+      label: '目标任务总数',
+      value: '73',
+      helper: '较上季度 +12%',
+      icon: 'solar:target-outline',
+      color: '#2563eb',
+      softColor: '#eff6ff',
+    },
+    {
+      label: '目标任务完成进度',
+      value: '30%',
+      helper: '较上周 +4.8%',
+      icon: 'solar:chart-2-outline',
+      color: '#0f9f72',
+      softColor: '#ecfdf5',
+    },
+    {
+      label: '重点战役项目数量',
+      value: '10',
+      helper: '本年度重点项目',
+      icon: 'solar:flag-2-outline',
+      color: '#7c3aed',
+      softColor: '#f5f3ff',
+    },
+    {
+      label: '项目里程碑达成率',
+      value: '20%',
+      helper: '共 25 个里程碑',
+      icon: 'solar:medal-ribbons-star-outline',
+      color: '#d97706',
+      softColor: '#fffbeb',
+    },
+  ]
+
+  const progressGroups = [
+    { label: '未开始', value: 28, color: '#cbd5e1' },
+    { label: '进行中', value: 23, color: '#60a5fa' },
+    { label: '已完成', value: 22, color: '#34d399' },
+  ]
+
+  const statusGroups = [
+    { label: '顺利', value: 35, color: '#10b981' },
+    { label: '落后', value: 1, color: '#f59e0b' },
+    { label: '推迟', value: 0, color: '#94a3b8' },
+    { label: '风险', value: 0, color: '#ef4444' },
+    { label: '完成', value: 0, color: '#6366f1' },
+    { label: '未开始', value: 269, color: '#cbd5e1' },
+  ]
+
+  const projects: ProjectItem[] = [
+    {
+      name: '客户体验升级专项',
+      owner: '林晓晨',
+      progress: 78,
+      deadline: '2026-06-30',
+      status: '正常',
+      description: '围绕核心业务链路,优化客户服务体验',
+    },
+    {
+      name: '智能运营平台建设',
+      owner: '周子涵',
+      progress: 62,
+      deadline: '2026-07-15',
+      status: '正常',
+      description: '统一数据资产与运营分析能力',
+    },
+    {
+      name: '供应链提效计划',
+      owner: '陈思远',
+      progress: 42,
+      deadline: '2026-08-20',
+      status: '滞后',
+      description: '通过流程重塑提升供应链协同效率',
+    },
+    {
+      name: '组织能力建设项目',
+      owner: '王一然',
+      progress: 24,
+      deadline: '2026-09-30',
+      status: '风险',
+      description: '完善人才发展与组织协作机制',
+    },
+    {
+      name: '年度品牌焕新项目',
+      owner: '赵可欣',
+      progress: 91,
+      deadline: '2026-05-28',
+      status: '已完成',
+      description: '完成品牌视觉和内容传播体系升级',
+    },
+  ]
+
+  const filteredProjects = computed(() => {
+    if (activeFilter.value === '我的项目') return projects.filter(item => item.owner === '林晓晨')
+    if (activeFilter.value === '风险项目') {
+      return projects.filter(item => item.status === '风险' || item.status === '滞后')
+    }
+    return projects
+  })
+
+  const statusClass = (status: ProjectStatus) => {
+    const classMap: Record<ProjectStatus, string> = {
+      正常: 'status-success',
+      滞后: 'status-warning',
+      风险: 'status-danger',
+      已完成: 'status-complete',
+    }
+    return classMap[status]
+  }
+
+  const ringStyle = (progress: number, color: string) => ({
+    background: `conic-gradient(${color} ${progress}%, #e5eaf0 0)`,
+  })
+
+  const maxStatusValue = Math.max(...statusGroups.map(item => item.value))
+
+  const barStyle = (value: number, color: string) => ({
+    height: `${Math.max((value / maxStatusValue) * 100, value ? 4 : 0)}%`,
+    background: color,
+  })
 </script>
+
 <template>
-  <div>
-    <el-card shadow="never">
-      <el-skeleton :loading="loading" animated>
-        <el-row :gutter="16" justify="space-between">
-          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
-            <div class="flex items-center">
-              <el-avatar :src="avatar" :size="70" class="mr-16px">
-                <img src="@/assets/imgs/avatar.gif" alt="" />
-              </el-avatar>
-              <div>
-                <div class="text-20px">
-                  {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
-                </div>
-                <div class="text-14px text-gray-500 mt-10px">
-                  {{ t('workplace.toady') }},20℃ - 32℃!
-                </div>
+  <main class="okr-dashboard">
+    <header class="page-header">
+      <div>
+        <div class="eyebrow"><span class="eyebrow-dot" /> OKR 工作台</div>
+        <h1>目标与项目总览</h1>
+        <p>你好,{{ username }}。这是 2026 年的目标执行概况。</p>
+      </div>
+      <div class="header-actions">
+        <div class="date-chip">
+          <Icon icon="solar:calendar-minimalistic-outline" :size="17" />
+          <span>2026 年 · 第二季度</span>
+          <Icon icon="solar:alt-arrow-down-outline" :size="15" />
+        </div>
+        <el-button type="primary" class="create-button">
+          <Icon icon="solar:add-circle-outline" :size="18" />
+          新建目标
+        </el-button>
+      </div>
+    </header>
+
+    <section class="metric-grid" aria-label="核心指标">
+      <article v-for="item in metrics" :key="item.label" class="metric-card">
+        <div class="metric-topline">
+          <span class="metric-label">{{ item.label }}</span>
+          <span class="metric-icon" :style="{ color: item.color, backgroundColor: item.softColor }">
+            <Icon :icon="item.icon" :size="20" />
+          </span>
+        </div>
+        <div class="metric-value">{{ item.value }}</div>
+        <div class="metric-helper" :style="{ color: item.color }">
+          <Icon icon="solar:arrow-right-up-outline" :size="14" />
+          {{ item.helper }}
+        </div>
+      </article>
+    </section>
+
+    <section class="insight-grid">
+      <article class="dashboard-card progress-card">
+        <div class="card-heading">
+          <div>
+            <div class="card-kicker">任务执行</div>
+            <h2>目标任务进度</h2>
+          </div>
+          <button class="icon-button" aria-label="查看任务进度">
+            <Icon icon="solar:menu-dots-outline" :size="20" />
+          </button>
+        </div>
+        <div class="progress-content">
+          <div class="ring-wrap large-ring">
+            <div class="progress-ring" :style="ringStyle(30, '#2563eb')">
+              <div class="ring-center">
+                <strong>30%</strong>
+                <span>平均进度</span>
               </div>
             </div>
-          </el-col>
-          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
-            <div class="flex h-70px items-center justify-end lt-sm:mt-10px">
-              <div class="px-8px text-right">
-                <div class="text-14px text-gray-400 mb-16px">{{ t('workplace.project') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.project"
-                  :duration="2600" />
-              </div>
-              <el-divider direction="vertical" />
-              <div class="px-8px text-right">
-                <div class="text-14px text-gray-400 mb-16px">{{ t('workplace.toDo') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.todo"
-                  :duration="2600" />
-              </div>
-              <el-divider direction="vertical" border-style="dashed" />
-              <div class="px-8px text-right">
-                <div class="text-14px text-gray-400 mb-16px">{{ t('workplace.access') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.access"
-                  :duration="2600" />
+          </div>
+          <div class="legend-list">
+            <div v-for="item in progressGroups" :key="item.label" class="legend-row">
+              <span class="legend-name"
+                ><i :style="{ background: item.color }" />{{ item.label }}</span
+              >
+              <strong>{{ item.value }}</strong>
+              <span class="legend-percent">{{ Math.round((item.value / 73) * 100) }}%</span>
+            </div>
+          </div>
+        </div>
+        <div class="card-footnote"><span class="footnote-dot blue" />较上月完成任务增加 8 项</div>
+      </article>
+
+      <article class="dashboard-card status-card">
+        <div class="card-heading">
+          <div>
+            <div class="card-kicker">项目健康度</div>
+            <h2>成果状态统计</h2>
+          </div>
+          <button class="icon-button" aria-label="查看成果状态">
+            <Icon icon="solar:menu-dots-outline" :size="20" />
+          </button>
+        </div>
+        <div class="status-content">
+          <div class="ring-wrap small-ring">
+            <div class="progress-ring" :style="ringStyle(97, '#10b981')">
+              <div class="ring-center">
+                <strong>97%</strong>
+                <span>顺利执行</span>
               </div>
             </div>
-          </el-col>
-        </el-row>
-      </el-skeleton>
-    </el-card>
-  </div>
-
-  <el-row class="mt-8px" :gutter="8" justify="space-between">
-    <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
-      <el-card shadow="never">
-        <template #header>
-          <div class="flex h-3 justify-between">
-            <span>{{ t('workplace.project') }}</span>
-            <el-link
-              type="primary"
-              underline="never"
-              href="https://github.com/yudaocode"
-              target="_blank">
-              {{ t('action.more') }}
-            </el-link>
           </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <el-row :gutter="8" class="gap-y-8px">
-            <el-col
-              v-for="(item, index) in projects"
-              :key="`card-${index}`"
-              :xl="8"
-              :lg="8"
-              :md="8"
-              :sm="24"
-              :xs="24"
-              class="!flex">
-              <el-card
-                shadow="hover"
-                class="flex-1 cursor-pointer"
-                body-class="flex h-full flex-col"
-                @click="handleProjectClick(item.message)">
-                <div class="flex items-center">
-                  <Icon
-                    :icon="item.icon"
-                    :size="25"
-                    class="mr-8px flex-none"
-                    :style="{ color: item.color }" />
-                  <span class="text-16px min-w-0 line-clamp-2" :title="item.name">{{
-                    item.name
-                  }}</span>
-                </div>
-                <div
-                  class="text-12px text-gray-400 mt-12px break-all line-clamp-2"
-                  :title="t(item.message)">
-                  {{ t(item.message) }}
-                </div>
-                <div
-                  class="text-12px text-gray-400 mt-auto pt-12px flex gap-8px items-center justify-between">
-                  <span class="min-w-0 truncate" :title="item.personal">{{ item.personal }}</span>
-                  <span class="shrink-0 whitespace-nowrap">
-                    {{ formatTime(item.time, 'yyyy-MM-dd') }}
-                  </span>
-                </div>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-
-      <el-card shadow="never" class="mt-8px">
-        <el-skeleton :loading="loading" animated>
-          <el-row :gutter="20" justify="space-between">
-            <el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-8px">
-                <el-skeleton :loading="loading" animated>
-                  <Echart :options="pieOptionsData" :height="280" />
-                </el-skeleton>
-              </el-card>
-            </el-col>
-            <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-8px">
-                <el-skeleton :loading="loading" animated>
-                  <Echart :options="barOptionsData" :height="280" />
-                </el-skeleton>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-    </el-col>
-    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
-      <el-card shadow="never">
-        <template #header>
-          <div class="flex h-3 justify-between">
-            <span>{{ t('workplace.shortcutOperation') }}</span>
+          <div class="status-list">
+            <div v-for="item in statusGroups.slice(0, 4)" :key="item.label" class="status-row">
+              <span class="legend-name"
+                ><i :style="{ background: item.color }" />{{ item.label }}</span
+              >
+              <strong>{{ item.value }}</strong>
+            </div>
+          </div>
+        </div>
+        <div class="status-summary"
+          ><span>当前执行项目</span><strong>35</strong><span>个</span></div
+        >
+      </article>
+
+      <article class="dashboard-card milestone-card">
+        <div class="card-heading">
+          <div>
+            <div class="card-kicker">关键节点</div>
+            <h2>里程碑达成</h2>
           </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <el-row>
-            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
-              <div class="flex items-center">
-                <Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
-                <el-link type="default" underline="never" @click="handleShortcutClick(item.url)">
-                  {{ item.name }}
-                </el-link>
+          <button class="icon-button" aria-label="查看里程碑">
+            <Icon icon="solar:menu-dots-outline" :size="20" />
+          </button>
+        </div>
+        <div class="milestone-content">
+          <div class="ring-wrap large-ring">
+            <div class="progress-ring" :style="ringStyle(20, '#d97706')">
+              <div class="ring-center">
+                <strong>20%</strong>
+                <span>平均达成率</span>
               </div>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-      <el-card shadow="never" class="mt-8px">
-        <template #header>
-          <div class="flex h-3 justify-between">
-            <span>{{ t('workplace.notice') }}</span>
-            <el-link type="primary" underline="never">{{ t('action.more') }}</el-link>
+            </div>
           </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
-            <div class="flex items-center">
-              <el-avatar :src="avatar" :size="35" class="mr-16px">
-                <img src="@/assets/imgs/avatar.gif" alt="" />
-              </el-avatar>
-              <div>
-                <div class="text-14px">
-                  <Highlight :keys="item.keys.map(v => t(v))">
-                    {{ item.type }} : {{ item.title }}
-                  </Highlight>
-                </div>
-                <div class="text-12px text-gray-400 mt-16px">
-                  {{ formatTime(item.date, 'yyyy-MM-dd') }}
-                </div>
+          <div class="milestone-detail">
+            <div class="milestone-number">5 <span>/ 25</span></div>
+            <div class="milestone-label">已完成里程碑</div>
+            <div class="mini-progress"><span style="width: 20%; background: #d97706" /></div>
+            <div class="milestone-hint">本月还有 3 个节点待完成</div>
+          </div>
+        </div>
+        <div class="card-footnote"
+          ><span class="footnote-dot amber" />距离季度目标还需完成 20 个节点</div
+        >
+      </article>
+    </section>
+
+    <section class="lower-grid">
+      <article class="dashboard-card chart-card">
+        <div class="card-heading">
+          <div>
+            <div class="card-kicker">执行分布</div>
+            <h2>成果状态统计</h2>
+          </div>
+          <div class="chart-total"><strong>305</strong><span>项总计</span></div>
+        </div>
+        <div class="bar-chart">
+          <div class="chart-y-axis"
+            ><span>300</span><span>200</span><span>100</span><span>0</span></div
+          >
+          <div class="chart-area">
+            <div class="chart-gridline grid-300" />
+            <div class="chart-gridline grid-200" />
+            <div class="chart-gridline grid-100" />
+            <div class="chart-gridline grid-0" />
+            <div class="bars">
+              <div v-for="item in statusGroups" :key="item.label" class="bar-item">
+                <span class="bar-value">{{ item.value }}</span>
+                <div class="bar-track"><i :style="barStyle(item.value, item.color)" /></div>
+                <span class="bar-label">{{ item.label }}</span>
               </div>
             </div>
-            <el-divider />
           </div>
-        </el-skeleton>
-      </el-card>
-    </el-col>
-  </el-row>
+        </div>
+      </article>
+
+      <article class="dashboard-card focus-card">
+        <div class="card-heading">
+          <div>
+            <div class="card-kicker">快速浏览</div>
+            <h2>本周重点关注</h2>
+          </div>
+          <span class="focus-count">3 个待处理</span>
+        </div>
+        <div class="focus-list">
+          <div class="focus-item">
+            <span class="focus-index danger">01</span>
+            <div><strong>供应链提效计划</strong><span>进度落后 8%,请及时跟进</span></div>
+            <Icon icon="solar:alt-arrow-right-outline" :size="18" />
+          </div>
+          <div class="focus-item">
+            <span class="focus-index warning">02</span>
+            <div><strong>组织能力建设项目</strong><span>本周有 2 个关键节点</span></div>
+            <Icon icon="solar:alt-arrow-right-outline" :size="18" />
+          </div>
+          <div class="focus-item">
+            <span class="focus-index success">03</span>
+            <div><strong>客户体验升级专项</strong><span>里程碑即将达成</span></div>
+            <Icon icon="solar:alt-arrow-right-outline" :size="18" />
+          </div>
+        </div>
+        <button class="text-action"
+          >查看全部动态 <Icon icon="solar:arrow-right-outline" :size="16"
+        /></button>
+      </article>
+    </section>
+
+    <section class="dashboard-card project-card">
+      <div class="table-heading">
+        <div>
+          <div class="card-kicker">项目管理</div>
+          <h2>2026 年重点战役项目</h2>
+          <p>持续跟进重点项目的目标、进度和执行状态</p>
+        </div>
+        <div class="table-actions">
+          <div class="filter-tabs">
+            <button
+              v-for="filter in filters"
+              :key="filter"
+              :class="{ active: activeFilter === filter }"
+              @click="activeFilter = filter">
+              {{ filter }}
+            </button>
+          </div>
+          <button class="outline-button"
+            ><Icon icon="solar:export-outline" :size="16" />导出</button
+          >
+        </div>
+      </div>
+      <div class="project-table-wrap">
+        <table class="project-table">
+          <thead>
+            <tr>
+              <th>项目名称</th>
+              <th>负责人</th>
+              <th class="progress-column">当前进度</th>
+              <th>截止日期</th>
+              <th>执行状态</th>
+              <th aria-label="操作" />
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="item in filteredProjects" :key="item.name">
+              <td>
+                <div class="project-name"
+                  ><span class="project-mark" /> <strong>{{ item.name }}</strong></div
+                >
+                <span class="project-description">{{ item.description }}</span>
+              </td>
+              <td
+                ><span class="owner"
+                  ><span class="owner-avatar">{{ item.owner.slice(0, 1) }}</span
+                  >{{ item.owner }}</span
+                ></td
+              >
+              <td class="progress-column">
+                <div class="table-progress"
+                  ><span><i :style="{ width: `${item.progress}%` }" /></span
+                  ><strong>{{ item.progress }}%</strong></div
+                >
+              </td>
+              <td class="date-cell">{{ item.deadline }}</td>
+              <td
+                ><span class="status-pill" :class="statusClass(item.status)"
+                  ><i />{{ item.status }}</span
+                ></td
+              >
+              <td
+                ><button class="row-more" aria-label="更多操作"
+                  ><Icon icon="solar:menu-dots-outline" :size="18" /></button
+              ></td>
+            </tr>
+          </tbody>
+        </table>
+        <div v-if="!filteredProjects.length" class="empty-state">暂无符合条件的项目</div>
+      </div>
+    </section>
+  </main>
 </template>
+
+<style lang="scss" scoped>
+  .okr-dashboard {
+    --ink: #172033;
+    --muted: #5f6b7d;
+    --line: #d8dee8;
+    --surface: #fff;
+    --background: #f4f6fa;
+
+    min-height: 100%;
+    padding: 28px 30px 42px;
+    color: var(--ink);
+    background: var(--background);
+  }
+
+  .page-header,
+  .metric-topline,
+  .card-heading,
+  .table-heading,
+  .header-actions,
+  .date-chip,
+  .metric-helper,
+  .project-name,
+  .owner,
+  .status-pill,
+  .text-action,
+  .outline-button {
+    display: flex;
+    align-items: center;
+  }
+
+  .page-header {
+    justify-content: space-between;
+    gap: 24px;
+    margin-bottom: 24px;
+
+    h1 {
+      margin: 7px 0 5px;
+      font-size: 26px;
+      font-weight: 720;
+      letter-spacing: -0.8px;
+      color: var(--ink);
+    }
+
+    p {
+      margin: 0;
+      font-size: 14px;
+      color: var(--muted);
+    }
+  }
+
+  .eyebrow,
+  .card-kicker {
+    font-size: 12px;
+    font-weight: 650;
+    letter-spacing: 0.08em;
+    color: #667387;
+    text-transform: uppercase;
+  }
+
+  .eyebrow {
+    display: flex;
+    align-items: center;
+    gap: 7px;
+    color: #2563eb;
+  }
+
+  .eyebrow-dot {
+    width: 7px;
+    height: 7px;
+    background: #2563eb;
+    border-radius: 50%;
+    box-shadow: 0 0 0 4px #dbeafe;
+  }
+
+  .header-actions {
+    gap: 12px;
+  }
+
+  .date-chip {
+    height: 38px;
+    padding: 0 12px;
+    font-size: 13px;
+    color: #4f5d70;
+    background: var(--surface);
+    border: 1px solid var(--line);
+    border-radius: 9px;
+    gap: 8px;
+  }
+
+  .create-button {
+    height: 38px;
+    padding: 0 15px;
+    font-size: 13px;
+    border: 0;
+    border-radius: 9px;
+    box-shadow: 0 5px 13px rgb(37 99 235 / 18%);
+
+    .iconify {
+      margin-right: 5px;
+    }
+  }
+
+  .metric-grid {
+    display: grid;
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+    gap: 14px;
+    margin-bottom: 14px;
+  }
+
+  .metric-card,
+  .dashboard-card {
+    background: var(--surface);
+    border: 1px solid var(--line);
+    border-radius: 12px;
+    box-shadow: 0 4px 15px rgb(29 43 68 / 3%);
+  }
+
+  .metric-card {
+    min-height: 137px;
+    padding: 18px 20px;
+  }
+
+  .metric-topline {
+    justify-content: space-between;
+  }
+
+  .metric-label {
+    font-size: 13px;
+    color: #536176;
+  }
+
+  .metric-icon {
+    display: grid;
+    width: 34px;
+    height: 34px;
+    place-items: center;
+    border-radius: 9px;
+  }
+
+  .metric-value {
+    margin-top: 14px;
+    font-size: 32px;
+    font-weight: 720;
+    letter-spacing: -1px;
+    color: var(--ink);
+  }
+
+  .metric-helper {
+    gap: 3px;
+    margin-top: 7px;
+    font-size: 12px;
+  }
+
+  .insight-grid {
+    display: grid;
+    grid-template-columns: 1.15fr 1fr 1.15fr;
+    gap: 14px;
+    margin-bottom: 14px;
+  }
+
+  .dashboard-card {
+    overflow: hidden;
+  }
+
+  .progress-card,
+  .status-card,
+  .milestone-card,
+  .chart-card,
+  .focus-card {
+    min-height: 250px;
+    padding: 21px 22px 17px;
+  }
+
+  .card-heading {
+    justify-content: space-between;
+
+    h2 {
+      margin: 5px 0 0;
+      font-size: 18px;
+      font-weight: 680;
+      letter-spacing: -0.25px;
+      color: var(--ink);
+    }
+  }
+
+  .icon-button,
+  .row-more {
+    display: grid;
+    padding: 0;
+    color: #687589;
+    cursor: pointer;
+    background: transparent;
+    border: 0;
+    place-items: center;
+  }
+
+  .progress-content,
+  .status-content,
+  .milestone-content {
+    display: flex;
+    align-items: center;
+    gap: 24px;
+    min-height: 146px;
+  }
+
+  .progress-content,
+  .milestone-content {
+    justify-content: space-around;
+  }
+
+  .ring-wrap {
+    display: grid;
+    flex: none;
+    place-items: center;
+  }
+
+  .large-ring {
+    width: 127px;
+    height: 127px;
+  }
+
+  .small-ring {
+    width: 124px;
+    height: 124px;
+  }
+
+  .progress-ring {
+    display: grid;
+    width: 100%;
+    height: 100%;
+    padding: 10px;
+    border-radius: 50%;
+    place-items: center;
+  }
+
+  .ring-center {
+    display: flex;
+    width: 100%;
+    height: 100%;
+    background: var(--surface);
+    border-radius: 50%;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    strong {
+      font-size: 21px;
+      font-weight: 720;
+      letter-spacing: -0.8px;
+      color: var(--ink);
+    }
+
+    span {
+      margin-top: 3px;
+      font-size: 11px;
+      color: #667387;
+    }
+  }
+
+  .legend-list,
+  .status-list {
+    flex: 1;
+  }
+
+  .legend-row,
+  .status-row {
+    display: grid;
+    min-height: 29px;
+    font-size: 12px;
+    color: #4f5d70;
+    grid-template-columns: minmax(74px, 1fr) 30px 38px;
+    align-items: center;
+    gap: 7px;
+
+    strong {
+      font-size: 13px;
+      font-weight: 650;
+      color: var(--ink);
+      text-align: right;
+    }
+  }
+
+  .legend-name {
+    display: flex;
+    align-items: center;
+    gap: 7px;
+    white-space: nowrap;
+
+    i {
+      width: 7px;
+      height: 7px;
+      flex: none;
+      border-radius: 50%;
+    }
+  }
+
+  .legend-percent {
+    font-size: 11px;
+    color: #6e7b8e;
+    text-align: right;
+  }
+
+  .card-footnote {
+    display: flex;
+    padding-top: 8px;
+    font-size: 11px;
+    color: #667387;
+    border-top: 1px solid #dfe4ec;
+    align-items: center;
+    gap: 6px;
+  }
+
+  .footnote-dot {
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+
+    &.blue {
+      background: #2563eb;
+    }
+
+    &.amber {
+      background: #d97706;
+    }
+  }
+
+  .status-content {
+    gap: 18px;
+    justify-content: center;
+  }
+
+  .status-list {
+    max-width: 160px;
+  }
+
+  .status-row {
+    grid-template-columns: 1fr 30px;
+    min-height: 27px;
+  }
+
+  .status-summary {
+    display: flex;
+    padding-top: 5px;
+    font-size: 11px;
+    color: #667387;
+    border-top: 1px solid #dfe4ec;
+    align-items: baseline;
+    gap: 5px;
+
+    strong {
+      font-size: 16px;
+      color: #10b981;
+    }
+  }
+
+  .milestone-detail {
+    min-width: 140px;
+  }
+
+  .milestone-number {
+    font-size: 25px;
+    font-weight: 720;
+    letter-spacing: -1px;
+    color: var(--ink);
+
+    span {
+      font-size: 14px;
+      font-weight: 500;
+      letter-spacing: 0;
+      color: #778397;
+    }
+  }
+
+  .milestone-label,
+  .milestone-hint {
+    font-size: 11px;
+    color: #687589;
+  }
+
+  .mini-progress {
+    width: 100%;
+    height: 6px;
+    margin: 17px 0 8px;
+    overflow: hidden;
+    background: #eee8dc;
+    border-radius: 10px;
+
+    span {
+      display: block;
+      height: 100%;
+      border-radius: inherit;
+    }
+  }
+
+  .lower-grid {
+    display: grid;
+    grid-template-columns: 1.35fr 1fr;
+    gap: 14px;
+    margin-bottom: 14px;
+  }
+
+  .chart-total {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+
+    strong {
+      font-size: 21px;
+      line-height: 1;
+      color: var(--ink);
+    }
+
+    span {
+      margin-top: 5px;
+      font-size: 11px;
+      color: #687589;
+    }
+  }
+
+  .bar-chart {
+    display: flex;
+    height: 190px;
+    padding-top: 17px;
+  }
+
+  .chart-y-axis {
+    display: flex;
+    width: 28px;
+    padding: 1px 0 25px;
+    font-size: 10px;
+    color: #6e7b8e;
+    text-align: left;
+    flex-direction: column;
+    justify-content: space-between;
+  }
+
+  .chart-area {
+    position: relative;
+    flex: 1;
+  }
+
+  .chart-gridline {
+    position: absolute;
+    right: 0;
+    left: 0;
+    height: 1px;
+    border-top: 1px dashed #d9e0e8;
+
+    &.grid-300 {
+      top: 0;
+    }
+
+    &.grid-200 {
+      top: 33.3%;
+    }
+
+    &.grid-100 {
+      top: 66.6%;
+    }
+
+    &.grid-0 {
+      bottom: 25px;
+      border-color: #dce2e9;
+      border-top-style: solid;
+    }
+  }
+
+  .bars {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    height: 100%;
+    justify-content: space-around;
+    gap: 12px;
+  }
+
+  .bar-item {
+    display: flex;
+    min-width: 29px;
+    flex: 1;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .bar-value {
+    height: 16px;
+    font-size: 11px;
+    color: #637083;
+  }
+
+  .bar-track {
+    display: flex;
+    width: 29px;
+    height: 130px;
+    align-items: flex-end;
+
+    i {
+      display: block;
+      width: 100%;
+      min-height: 0;
+      border-radius: 4px 4px 0 0;
+    }
+  }
+
+  .bar-label {
+    margin-top: 9px;
+    font-size: 11px;
+    color: #5f6d80;
+    white-space: nowrap;
+  }
+
+  .focus-count {
+    padding: 5px 8px;
+    font-size: 10px;
+    color: #d97706;
+    background: #fffbeb;
+    border-radius: 6px;
+  }
+
+  .focus-list {
+    margin-top: 16px;
+  }
+
+  .focus-item {
+    display: grid;
+    grid-template-columns: 27px 1fr 18px;
+    align-items: center;
+    gap: 10px;
+    padding: 10px 0;
+    border-bottom: 1px solid #f0f2f5;
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    strong,
+    span {
+      display: block;
+    }
+
+    strong {
+      overflow: hidden;
+      font-size: 12px;
+      font-weight: 600;
+      color: var(--ink);
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    div span {
+      margin-top: 3px;
+      font-size: 11px;
+      color: #667387;
+    }
+
+    > .iconify {
+      color: #738095;
+    }
+  }
+
+  .focus-index {
+    display: grid;
+    width: 27px;
+    height: 27px;
+    font-size: 10px;
+    font-weight: 650;
+    border-radius: 7px;
+    place-items: center;
+
+    &.danger {
+      color: #ef4444;
+      background: #fef2f2;
+    }
+
+    &.warning {
+      color: #d97706;
+      background: #fffbeb;
+    }
+
+    &.success {
+      color: #059669;
+      background: #ecfdf5;
+    }
+  }
+
+  .text-action {
+    padding: 0;
+    margin-top: 10px;
+    font-size: 12px;
+    color: #2563eb;
+    cursor: pointer;
+    background: transparent;
+    border: 0;
+    gap: 5px;
+  }
+
+  .project-card {
+    padding: 22px 22px 5px;
+  }
+
+  .table-heading {
+    justify-content: space-between;
+    gap: 20px;
+    margin-bottom: 18px;
+
+    h2 {
+      margin: 5px 0 4px;
+      font-size: 17px;
+      font-weight: 680;
+      color: var(--ink);
+    }
+
+    p {
+      margin: 0;
+      font-size: 12px;
+      color: #667387;
+    }
+  }
+
+  .table-actions {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+
+  .filter-tabs {
+    display: flex;
+    padding: 3px;
+    background: #f5f7fa;
+    border-radius: 8px;
+
+    button {
+      padding: 6px 10px;
+      font-size: 11px;
+      color: #667387;
+      cursor: pointer;
+      background: transparent;
+      border: 0;
+      border-radius: 6px;
+
+      &.active {
+        color: #2563eb;
+        background: var(--surface);
+        box-shadow: 0 1px 3px rgb(30 41 59 / 8%);
+      }
+    }
+  }
+
+  .outline-button {
+    height: 30px;
+    padding: 0 10px;
+    font-size: 11px;
+    color: #536176;
+    cursor: pointer;
+    background: var(--surface);
+    border: 1px solid var(--line);
+    border-radius: 7px;
+    gap: 5px;
+  }
+
+  .project-table-wrap {
+    overflow-x: auto;
+  }
+
+  .project-table {
+    width: 100%;
+    min-width: 760px;
+    border-collapse: collapse;
+    table-layout: fixed;
+
+    th,
+    td {
+      padding: 12px 10px;
+      text-align: left;
+      border-top: 1px solid #eef1f4;
+    }
+
+    th {
+      font-size: 12px;
+      font-weight: 550;
+      color: #637083;
+    }
+
+    td {
+      font-size: 12px;
+      color: #44546a;
+    }
+
+    th:first-child,
+    td:first-child {
+      width: 29%;
+    }
+
+    th:nth-child(2),
+    td:nth-child(2) {
+      width: 15%;
+    }
+
+    th:nth-child(3),
+    td:nth-child(3) {
+      width: 22%;
+    }
+
+    th:nth-child(4),
+    td:nth-child(4) {
+      width: 15%;
+    }
+
+    th:nth-child(5),
+    td:nth-child(5) {
+      width: 13%;
+    }
+
+    th:last-child,
+    td:last-child {
+      width: 6%;
+      text-align: right;
+    }
+  }
+
+  .project-name {
+    gap: 8px;
+    color: var(--ink);
+
+    strong {
+      overflow: hidden;
+      font-size: 12px;
+      font-weight: 600;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  .project-mark {
+    width: 7px;
+    height: 22px;
+    background: #2563eb;
+    border-radius: 4px;
+    flex: none;
+  }
+
+  .project-description {
+    display: block;
+    margin: 5px 0 0 15px;
+    overflow: hidden;
+    font-size: 11px;
+    color: #667387;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .owner {
+    gap: 7px;
+    color: #4f5d70;
+    white-space: nowrap;
+  }
+
+  .owner-avatar {
+    display: grid;
+    width: 24px;
+    height: 24px;
+    font-size: 11px;
+    color: #2563eb;
+    background: #eff6ff;
+    border-radius: 50%;
+    place-items: center;
+  }
+
+  .table-progress {
+    display: flex;
+    align-items: center;
+    gap: 9px;
+
+    > span {
+      display: block;
+      width: 95px;
+      height: 6px;
+      overflow: hidden;
+      background: #eef1f5;
+      border-radius: 10px;
+
+      i {
+        display: block;
+        height: 100%;
+        background: #2563eb;
+        border-radius: inherit;
+      }
+    }
+
+    strong {
+      font-size: 11px;
+      font-weight: 600;
+      color: #536176;
+    }
+  }
+
+  .date-cell {
+    color: #667387;
+    font-variant-numeric: tabular-nums;
+  }
+
+  .status-pill {
+    width: max-content;
+    padding: 5px 8px;
+    font-size: 11px;
+    border-radius: 6px;
+    gap: 5px;
+
+    i {
+      width: 5px;
+      height: 5px;
+      border-radius: 50%;
+    }
+
+    &.status-success {
+      color: #059669;
+      background: #ecfdf5;
+
+      i {
+        background: #10b981;
+      }
+    }
+
+    &.status-warning {
+      color: #b45309;
+      background: #fffbeb;
+
+      i {
+        background: #f59e0b;
+      }
+    }
+
+    &.status-danger {
+      color: #dc2626;
+      background: #fef2f2;
+
+      i {
+        background: #ef4444;
+      }
+    }
+
+    &.status-complete {
+      color: #4f46e5;
+      background: #eef2ff;
+
+      i {
+        background: #6366f1;
+      }
+    }
+  }
+
+  .empty-state {
+    padding: 28px;
+    font-size: 13px;
+    color: #667387;
+    text-align: center;
+  }
+
+  @media (width <= 1200px) {
+    .metric-grid {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+
+    .insight-grid {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+
+    .milestone-card {
+      grid-column: span 2;
+    }
+  }
+
+  @media (width <= 860px) {
+    .okr-dashboard {
+      padding: 20px 16px 30px;
+    }
+
+    .page-header,
+    .table-heading {
+      align-items: flex-start;
+      flex-direction: column;
+    }
+
+    .header-actions,
+    .table-actions {
+      width: 100%;
+      justify-content: space-between;
+    }
+
+    .date-chip {
+      flex: 1;
+    }
+
+    .lower-grid,
+    .insight-grid {
+      grid-template-columns: 1fr;
+    }
+
+    .milestone-card {
+      grid-column: auto;
+    }
+  }
+
+  @media (width <= 560px) {
+    .metric-grid {
+      grid-template-columns: 1fr;
+    }
+
+    .metric-card {
+      min-height: 118px;
+    }
+
+    .progress-content,
+    .status-content,
+    .milestone-content {
+      gap: 13px;
+    }
+
+    .large-ring,
+    .small-ring {
+      width: 105px;
+      height: 105px;
+    }
+
+    .table-actions {
+      align-items: stretch;
+      flex-direction: column;
+    }
+
+    .filter-tabs {
+      justify-content: space-between;
+    }
+
+    .filter-tabs button {
+      flex: 1;
+    }
+
+    .outline-button {
+      justify-content: center;
+    }
+  }
+</style>