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 (