From 2881b866a62aeb5f05955ac0598093a0c31c3f1f Mon Sep 17 00:00:00 2001 From: simonkoson <28867558@qq.com> Date: Mon, 25 May 2026 13:45:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20KPI=E7=BB=86=E6=9D=A1=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=A4=E9=A1=B9=E5=AE=8C=E6=88=90=E7=8E=87=20+=20=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E8=8F=9C=E5=8D=95=E5=8A=A0=E4=B8=A4=E4=B8=AA=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Layout/SideNav.css | 26 ++++++++++++- frontend/src/components/Layout/SideNav.jsx | 23 ++++++++++++ frontend/src/pages/Dashboard/Dashboard.jsx | 43 +++++++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Layout/SideNav.css b/frontend/src/components/Layout/SideNav.css index 5686a48..1fdd82b 100644 --- a/frontend/src/components/Layout/SideNav.css +++ b/frontend/src/components/Layout/SideNav.css @@ -35,4 +35,28 @@ .side-nav-menu .ant-menu-item-selected { background: var(--color-accent-green) !important; color: var(--color-primary-green) !important; -} \ No newline at end of file +} + +/* disabled 项整体灰化 + 不可点击 */ +.side-nav-menu .ant-menu-item-disabled { + opacity: 0.55; + cursor: not-allowed !important; +} + +/* 即将上线小标签 */ +.menu-soon-tag { + font-size: 10px; + color: #999; + background: #f0f0f0; + padding: 1px 5px; + border-radius: 4px; + margin-left: 6px; + vertical-align: middle; + white-space: nowrap; +} + +/* disabled 项内标签不受 opacity 影响 */ +.side-nav-menu .ant-menu-item-disabled .menu-soon-tag { + opacity: 1; + color: #888; +} diff --git a/frontend/src/components/Layout/SideNav.jsx b/frontend/src/components/Layout/SideNav.jsx index 5b53d19..0cb6b86 100644 --- a/frontend/src/components/Layout/SideNav.jsx +++ b/frontend/src/components/Layout/SideNav.jsx @@ -7,6 +7,7 @@ import { SyncOutlined, UserOutlined, TeamOutlined, + SoundOutlined, } from '@ant-design/icons' import useAuthStore from '../../stores/authStore' @@ -23,6 +24,28 @@ function SideNav() { { key: '/doco', icon: , label: '文稿对齐' }, { key: '/editor-home', icon: , label: '个人首页' }, { key: '/users', icon: , label: '用户管理' }, + { + key: 'tts-placeholder', + icon: , + label: ( + + 蓝皓配音 TTS 2.0 + 即将上线 + + ), + disabled: true, + }, + { + key: 'collab-placeholder', + icon: , + label: ( + + 内部协作(Mattermost) + 即将上线 + + ), + disabled: true, + }, ] // 按角色过滤菜单项 diff --git a/frontend/src/pages/Dashboard/Dashboard.jsx b/frontend/src/pages/Dashboard/Dashboard.jsx index 498f208..25696c1 100644 --- a/frontend/src/pages/Dashboard/Dashboard.jsx +++ b/frontend/src/pages/Dashboard/Dashboard.jsx @@ -7,6 +7,8 @@ import { BarChartOutlined, PictureOutlined, UploadOutlined, + TargetOutlined, + AimOutlined, } from '@ant-design/icons' import useAuthStore from '../../stores/authStore' import { listEpisodes } from '../../services/episodeService' @@ -54,6 +56,32 @@ function getShortTitle(title) { return title.length > 9 ? title.slice(0, 9) + '...' : title } +/** + * 计算当年完成率(纯前端,量纲已确认为 0~1 小数直接相除) + * - "当年"= 当前自然年(new Date().getFullYear()) + * - avg(audience_share) 当年已录期 ÷ base/stretch_target + * - 无数据时始终返回 { base: '—', stretch: '—' } + * - 完成率可 >100% 正常显示 + */ +function calcCompletionRate(episodes, targets) { + const yearInt = new Date().getFullYear() + const yearEpisodes = episodes.filter(e => { + const airYear = new Date(e.air_date).getFullYear() + return airYear === yearInt && e.audience_share != null + }) + const target = (targets || []).find(t => Number(t.year) === yearInt) + + if (!yearEpisodes.length || !target) return { base: '—', stretch: '—' } + + const avgShare = yearEpisodes.reduce((sum, e) => sum + Number(e.audience_share), 0) / yearEpisodes.length + const baseRate = avgShare / Number(target.base_target) + const stretchRate = avgShare / Number(target.stretch_target) + return { + base: (baseRate * 100).toFixed(1) + '%', + stretch: (stretchRate * 100).toFixed(1) + '%', + } +} + function Dashboard() { const { user } = useAuthStore() const [episodes, setEpisodes] = useState([]) @@ -95,7 +123,8 @@ function Dashboard() { setCoverModalOpen(false) } - // KPI 数据 + // KPI 数据(扩为 5 项:原有 3 项 + 年度完成率 2 项) + const completion = calcCompletionRate(episodes, targets) const kpiData = [ { icon: , @@ -115,6 +144,18 @@ function Dashboard() { label: '年度目标', value: targets.length || '--', }, + { + icon: , + bg: '#f3e5f5', + label: '基础目标完成率', + value: completion.base, + }, + { + icon: , + bg: '#fce4ec', + label: '摸高目标完成率', + value: completion.stretch, + }, ] return (