feat: KPI细条新增两项完成率 + 侧边菜单加两个占位项
This commit is contained in:
@@ -36,3 +36,27 @@
|
|||||||
background: var(--color-accent-green) !important;
|
background: var(--color-accent-green) !important;
|
||||||
color: var(--color-primary-green) !important;
|
color: var(--color-primary-green) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
|
SoundOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import useAuthStore from '../../stores/authStore'
|
import useAuthStore from '../../stores/authStore'
|
||||||
|
|
||||||
@@ -23,6 +24,28 @@ function SideNav() {
|
|||||||
{ key: '/doco', icon: <SyncOutlined />, label: '文稿对齐' },
|
{ key: '/doco', icon: <SyncOutlined />, label: '文稿对齐' },
|
||||||
{ key: '/editor-home', icon: <UserOutlined />, label: '个人首页' },
|
{ key: '/editor-home', icon: <UserOutlined />, label: '个人首页' },
|
||||||
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
|
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
|
||||||
|
{
|
||||||
|
key: 'tts-placeholder',
|
||||||
|
icon: <SoundOutlined />,
|
||||||
|
label: (
|
||||||
|
<span className="menu-item-with-tag">
|
||||||
|
蓝皓配音 TTS 2.0
|
||||||
|
<span className="menu-soon-tag">即将上线</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'collab-placeholder',
|
||||||
|
icon: <TeamOutlined />,
|
||||||
|
label: (
|
||||||
|
<span className="menu-item-with-tag">
|
||||||
|
内部协作(Mattermost)
|
||||||
|
<span className="menu-soon-tag">即将上线</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 按角色过滤菜单项
|
// 按角色过滤菜单项
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
PictureOutlined,
|
PictureOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
|
TargetOutlined,
|
||||||
|
AimOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import useAuthStore from '../../stores/authStore'
|
import useAuthStore from '../../stores/authStore'
|
||||||
import { listEpisodes } from '../../services/episodeService'
|
import { listEpisodes } from '../../services/episodeService'
|
||||||
@@ -54,6 +56,32 @@ function getShortTitle(title) {
|
|||||||
return title.length > 9 ? title.slice(0, 9) + '...' : 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() {
|
function Dashboard() {
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
const [episodes, setEpisodes] = useState([])
|
const [episodes, setEpisodes] = useState([])
|
||||||
@@ -95,7 +123,8 @@ function Dashboard() {
|
|||||||
setCoverModalOpen(false)
|
setCoverModalOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KPI 数据
|
// KPI 数据(扩为 5 项:原有 3 项 + 年度完成率 2 项)
|
||||||
|
const completion = calcCompletionRate(episodes, targets)
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
icon: <EyeOutlined style={{ color: '#6b8e6b', fontSize: 14 }} />,
|
icon: <EyeOutlined style={{ color: '#6b8e6b', fontSize: 14 }} />,
|
||||||
@@ -115,6 +144,18 @@ function Dashboard() {
|
|||||||
label: '年度目标',
|
label: '年度目标',
|
||||||
value: targets.length || '--',
|
value: targets.length || '--',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <TargetOutlined style={{ color: '#7b5e9e', fontSize: 14 }} />,
|
||||||
|
bg: '#f3e5f5',
|
||||||
|
label: '基础目标完成率',
|
||||||
|
value: completion.base,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <AimOutlined style={{ color: '#c0584f', fontSize: 14 }} />,
|
||||||
|
bg: '#fce4ec',
|
||||||
|
label: '摸高目标完成率',
|
||||||
|
value: completion.stretch,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user