From a7a793bbe75279fd535342019ed9c27e8db9e0f8 Mon Sep 17 00:00:00 2001 From: lowmaster-chen <1119780489@qq.com> Date: Mon, 29 Jun 2026 11:59:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=B7=A6=E4=BE=A7=E5=AF=BC=E8=88=AA=E5=88=86=E7=BB=84=E5=B1=82?= =?UTF-8?q?=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改了什么:按看板、奖励审核、数据配置重新组织左侧导航,二级入口展示在一级标题右下方;补齐主干大盘页缺失的类型字段,保证类型检查通过。 验证方式:npx tsc --noEmit;浏览器检查 /dashboard 左侧导航顺序和二级缩进。 --- src/app/(main)/layout.tsx | 272 ++++++++++++++++++++++++++++++++++---- src/lib/types.ts | 78 ++++++++++- 2 files changed, 319 insertions(+), 31 deletions(-) diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 6aac954..32d351a 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -11,7 +11,6 @@ import { HeartOutlined, LogoutOutlined, MessageOutlined, - MobileOutlined, MoneyCollectOutlined, NotificationOutlined, ProfileOutlined, @@ -20,25 +19,65 @@ import { TeamOutlined, UserOutlined, } from '@ant-design/icons'; -import { Avatar, Dropdown, Layout, Menu } from 'antd'; +import { Avatar, Dropdown, Layout, Tooltip } from 'antd'; import { clearAuth, getAdmin, getToken } from '@/lib/auth'; import type { AdminInfo } from '@/lib/types'; const { Sider, Header, Content } = Layout; -const MENU = [ - { key: '/dashboard', icon: , label: '数据大盘' }, - { key: '/users', icon: , label: '用户管理' }, - { key: '/devices', icon: , label: '设备管理' }, - { key: '/device-liveness', icon: , label: '设备存活' }, - { key: '/withdraws', icon: , label: '提现管理' }, - { key: '/price-reports', icon: , label: '上报审核' }, - { key: '/comparison-records', icon: , label: '比价记录' }, - { key: '/feedbacks', icon: , label: '反馈工单' }, - { key: '/ad-revenue', icon: , label: '广告配置' }, - { key: '/ad-revenue-report', icon: , label: '广告收益' }, - { key: '/cps', icon: , label: 'CPS 分发' }, - { key: '/config', icon: , label: '系统配置' }, +type NavItem = { + key: string; + icon: React.ReactNode; + label: string; + superOnly?: boolean; +}; + +type NavGroup = + | (NavItem & { children?: never }) + | { + key: string; + icon: React.ReactNode; + label: string; + children: NavItem[]; + superOnly?: boolean; + }; + +const hasChildren = (group: NavGroup): group is NavGroup & { children: NavItem[] } => + Array.isArray(group.children); + +const NAV_GROUPS: NavGroup[] = [ + { + key: 'dashboard', + icon: , + label: '看板', + children: [ + { key: '/dashboard', icon: , label: '数据大盘' }, + { key: '/ad-revenue-report', icon: , label: '广告收益' }, + { key: '/comparison-records', icon: , label: '比价记录' }, + { key: '/cps', icon: , label: 'CPS收益' }, + { key: '/device-liveness', icon: , label: '设备存活' }, + ], + }, + { + key: 'reward-review', + icon: , + label: '奖励审核', + children: [ + { key: '/withdraws', icon: , label: '提现审核' }, + { key: '/price-reports', icon: , label: '低价审核' }, + { key: '/feedbacks', icon: , label: '用户反馈' }, + ], + }, + { + key: 'data-config', + icon: , + label: '数据配置', + children: [ + { key: '/config', icon: , label: '系统配置' }, + { key: '/ad-revenue', icon: , label: '广告配置' }, + { key: '/users', icon: , label: '用户管理' }, + ], + }, { key: '/admins', icon: , label: '管理员', superOnly: true }, { key: '/event-logs', icon: , label: '埋点日志' }, { key: '/audit-logs', icon: , label: '审计日志' }, @@ -48,6 +87,7 @@ export default function MainLayout({ children }: { children: React.ReactNode }) const router = useRouter(); const pathname = usePathname(); const [admin, setAdmin] = useState(null); + const [collapsed, setCollapsed] = useState(false); useEffect(() => { if (!getToken()) { @@ -59,14 +99,19 @@ export default function MainLayout({ children }: { children: React.ReactNode }) if (!admin) return null; // 守卫期间不闪烁内容 - const items = MENU.filter((m) => !m.superOnly || admin.role === 'super_admin').map((m) => ({ - key: m.key, - icon: m.icon, - label: m.label, - })); - // 选中态:取路径一级(/users/123 → /users) const selectedKey = '/' + (pathname.split('/')[1] || 'dashboard'); + const canShow = (item: { superOnly?: boolean }) => !item.superOnly || admin.role === 'super_admin'; + const visibleGroups = NAV_GROUPS + .filter(canShow) + .map((group) => { + if (!hasChildren(group)) return group; + return { ...group, children: group.children.filter(canShow) }; + }) + .filter((group) => !hasChildren(group) || group.children.length > 0); + const flatNavItems = visibleGroups.flatMap((group) => + hasChildren(group) ? group.children : [group], + ); const logout = () => { clearAuth(); @@ -75,7 +120,15 @@ export default function MainLayout({ children }: { children: React.ReactNode }) return ( - + 运营后台 - router.push(key)} - /> + + {collapsed + ? flatNavItems.map((item) => ( + + router.push(item.key)} + aria-label={item.label} + > + {item.icon} + + + )) + : visibleGroups.map((group) => { + const isGroup = hasChildren(group); + const groupSelected = isGroup + ? group.children.some((child) => child.key === selectedKey) + : selectedKey === group.key; + if (!isGroup) { + return ( + router.push(group.key)} + > + {group.icon} + {group.label} + + ); + } + return ( + + + {group.icon} + {group.label} + + + {group.children.map((child) => ( + router.push(child.key)} + > + {child.icon} + {child.label} + + ))} + + + ); + })} + {children} + ); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 7de55c7..869f256 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -391,6 +391,12 @@ export interface AdRevenueTypeStat { revenue_yuan: number; // 该类型预估收益合计(元) } +export interface AdRevenueTypeBreakdown extends AdRevenueTypeStat { + ad_type: string; + expected_coin: number; + actual_coin: number; +} + // 广告收益报表:一次广告事件(逐条一行)。激励视频展示+发奖按 ad_session_id 合并;信息流展示/发奖各自成行。 export interface AdRevenueRow { event_key: string; // 事件稳定唯一键(前端 rowKey) @@ -436,9 +442,52 @@ export interface AdRevenueReport { total_expected_coin: number; total_actual_coin: number; mismatch_count: number; // 应发≠实发的发奖条数 + by_ad_type?: Record; items: AdRevenueRow[]; } +export interface DashboardPeriodTrendPoint { + date: string; + active_users: number; + new_users: number; + comparisons: number; +} + +export interface DashboardPeriodStats { + date_from: string; + date_to: string; + users: { + new: number; + active: number; + retained_new_users: number; + retention_rate: number | null; + retention_note: string; + }; + comparison: { + total: number; + success: number; + success_rate: number; + ordered: number; + average_duration_ms: number | null; + average_saved_cents: number | null; + }; + coins: { + granted_total: number; + reward_video_coin_total: number; + feed_ad_coin_total: number; + signin_coin_total: number; + signin_boost_coin_total: number; + task_coin_total: number; + coupon_reward_coin_total: number; + comparison_reward_coin_total: number; + regular_task_coin_total: number; + }; + cash: { + withdraw_success_cents: number; + }; + trend: DashboardPeriodTrendPoint[]; +} + export interface DashboardOverview { users: { total: number; @@ -448,7 +497,17 @@ export interface DashboardOverview { new_today: number; dau: number; }; - coins: { granted_total: number }; + coins: { + granted_total: number; + reward_video_coin_total?: number; + reward_video_watch_count?: number; + feed_ad_coin_total?: number; + feed_ad_watch_count?: number; + signin_coin_total?: number; + signin_count?: number; + signin_boost_coin_total?: number; + signin_boost_watch_count?: number; + }; cash: { withdraw_success_cents: number; withdraw_pending_count: number; @@ -456,8 +515,23 @@ export interface DashboardOverview { withdraw_failed_count: number; }; comparison: { total: number; success: number; success_rate: number }; + period: DashboardPeriodStats; feedback: { new: number }; - cps: { available: boolean; note: string }; + cps: { + available: boolean; + note: string; + meituan_order_count?: number; + meituan_commission_cents?: number; + meituan_hit_count?: number; + meituan_miss_count?: number; + meituan_unknown_rate_count?: number; + meituan_hit_rate?: number | null; + jd_order_count?: number; + jd_commission_cents?: number; + jd_actual_commission_cents?: number; + jd_estimated_commission_cents?: number; + jd_invalid_count?: number; + }; } export interface PriceReport { -- 2.52.0