|
|
@@ -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>
|