diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 688b950..2562a2d 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -563,6 +563,7 @@ export function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> @@ -591,6 +592,7 @@ export function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> + } + /> } /> - + ) @@ -78,7 +78,7 @@ export function AdminBackupsPage() {
diff --git a/frontend/src/features/admin/AdminChatsPage.tsx b/frontend/src/features/admin/AdminChatsPage.tsx index de26836..1ef8ad3 100644 --- a/frontend/src/features/admin/AdminChatsPage.tsx +++ b/frontend/src/features/admin/AdminChatsPage.tsx @@ -215,7 +215,7 @@ export function AdminChatsPage() { if (!canView) { return (
- +
) @@ -225,7 +225,7 @@ export function AdminChatsPage() {
diff --git a/frontend/src/features/admin/AdminCrashCategoriesPage.tsx b/frontend/src/features/admin/AdminCrashCategoriesPage.tsx index d6c7fda..b01ec1a 100644 --- a/frontend/src/features/admin/AdminCrashCategoriesPage.tsx +++ b/frontend/src/features/admin/AdminCrashCategoriesPage.tsx @@ -172,7 +172,7 @@ export function AdminCrashCategoriesPage() { if (!canView) { return (
- +
) @@ -182,7 +182,7 @@ export function AdminCrashCategoriesPage() {
共 {categoriesQuery.data?.length ?? 0} 个分类} extra={ diff --git a/frontend/src/features/admin/AdminCrashConfigPage.tsx b/frontend/src/features/admin/AdminCrashConfigPage.tsx index b2c613a..8a83b42 100644 --- a/frontend/src/features/admin/AdminCrashConfigPage.tsx +++ b/frontend/src/features/admin/AdminCrashConfigPage.tsx @@ -35,8 +35,8 @@ export function AdminCrashConfigPage() { if (!canView) { return (
- - + +
) } @@ -45,7 +45,7 @@ export function AdminCrashConfigPage() {
- - + +
) } @@ -220,7 +220,7 @@ export function AdminCrashOrdersPage() {
共 {ordersQuery.data?.total ?? 0} 单} extra={ diff --git a/frontend/src/features/admin/AdminCrashProductsPage.tsx b/frontend/src/features/admin/AdminCrashProductsPage.tsx index 277a9f8..ebdee23 100644 --- a/frontend/src/features/admin/AdminCrashProductsPage.tsx +++ b/frontend/src/features/admin/AdminCrashProductsPage.tsx @@ -245,8 +245,8 @@ export function AdminCrashProductsPage() { if (!canView) { return (
- - + +
) } @@ -255,7 +255,7 @@ export function AdminCrashProductsPage() {
共 {productsQuery.data?.total ?? 0} 个商品} extra={ diff --git a/frontend/src/features/admin/AdminDisputesPage.tsx b/frontend/src/features/admin/AdminDisputesPage.tsx index 6602252..7140e0e 100644 --- a/frontend/src/features/admin/AdminDisputesPage.tsx +++ b/frontend/src/features/admin/AdminDisputesPage.tsx @@ -173,7 +173,7 @@ export function AdminDisputesPage() {
diff --git a/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx b/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx index ce55076..4fea9a2 100644 --- a/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx +++ b/frontend/src/features/admin/AdminDistributionQrCodesPage.tsx @@ -504,7 +504,7 @@ export function AdminDistributionQrCodesPage() {
diff --git a/frontend/src/features/admin/AdminFinanceDetailsPage.tsx b/frontend/src/features/admin/AdminFinanceDetailsPage.tsx index 3039d78..26ee780 100644 --- a/frontend/src/features/admin/AdminFinanceDetailsPage.tsx +++ b/frontend/src/features/admin/AdminFinanceDetailsPage.tsx @@ -165,7 +165,7 @@ export function AdminFinanceDetailsPage() { if (!canView) { return (
- +
) @@ -175,7 +175,7 @@ export function AdminFinanceDetailsPage() {
@@ -256,7 +256,7 @@ export function AdminFinanceDetailsPage() { type="error" showIcon className="admin-table-alert" - message="资金明细加载失败" + message="财务明细加载失败" description={errorText(detailsQuery.error)} /> ) : null} @@ -273,7 +273,7 @@ export function AdminFinanceDetailsPage() { setPage(nextPage) setPageSize(nextSize) }} - emptyDescription="所选区间还没有资金明细" + emptyDescription="所选区间还没有财务明细" />
) diff --git a/frontend/src/features/admin/AdminFinancePage.tsx b/frontend/src/features/admin/AdminFinancePage.tsx index dbcc7e5..04d272c 100644 --- a/frontend/src/features/admin/AdminFinancePage.tsx +++ b/frontend/src/features/admin/AdminFinancePage.tsx @@ -265,7 +265,7 @@ export function AdminFinancePage() {
diff --git a/frontend/src/features/admin/AdminInvitationsPage.tsx b/frontend/src/features/admin/AdminInvitationsPage.tsx index add44a1..1c1d9c3 100644 --- a/frontend/src/features/admin/AdminInvitationsPage.tsx +++ b/frontend/src/features/admin/AdminInvitationsPage.tsx @@ -240,7 +240,7 @@ export function AdminInvitationsPage() { if (!canView) { return (
- +
) @@ -252,7 +252,7 @@ export function AdminInvitationsPage() {
diff --git a/frontend/src/features/admin/AdminManualEntriesPage.tsx b/frontend/src/features/admin/AdminManualEntriesPage.tsx index bdda9bd..bd5e0eb 100644 --- a/frontend/src/features/admin/AdminManualEntriesPage.tsx +++ b/frontend/src/features/admin/AdminManualEntriesPage.tsx @@ -366,8 +366,8 @@ export function AdminManualEntriesPage() { if (!canView) { return (
- - + +
) } @@ -376,7 +376,7 @@ export function AdminManualEntriesPage() {
diff --git a/frontend/src/features/admin/AdminRolesPage.tsx b/frontend/src/features/admin/AdminRolesPage.tsx index 008c871..66625fa 100644 --- a/frontend/src/features/admin/AdminRolesPage.tsx +++ b/frontend/src/features/admin/AdminRolesPage.tsx @@ -270,7 +270,7 @@ export function AdminRolesPage() {
diff --git a/frontend/src/features/admin/AdminStaffAccountsPage.tsx b/frontend/src/features/admin/AdminStaffAccountsPage.tsx index 99335bb..8971a2c 100644 --- a/frontend/src/features/admin/AdminStaffAccountsPage.tsx +++ b/frontend/src/features/admin/AdminStaffAccountsPage.tsx @@ -328,7 +328,7 @@ export function AdminStaffAccountsPage() { if (!canManage) { return (
- +
) @@ -338,7 +338,7 @@ export function AdminStaffAccountsPage() {
diff --git a/frontend/src/features/admin/AdminSupportGroupsPage.tsx b/frontend/src/features/admin/AdminSupportGroupsPage.tsx index 9f7087b..c9887be 100644 --- a/frontend/src/features/admin/AdminSupportGroupsPage.tsx +++ b/frontend/src/features/admin/AdminSupportGroupsPage.tsx @@ -238,7 +238,7 @@ export function AdminSupportGroupsPage() {
diff --git a/frontend/src/features/admin/AdminTrackingLinksPage.tsx b/frontend/src/features/admin/AdminTrackingLinksPage.tsx index 0a4babb..6ddbe67 100644 --- a/frontend/src/features/admin/AdminTrackingLinksPage.tsx +++ b/frontend/src/features/admin/AdminTrackingLinksPage.tsx @@ -322,8 +322,8 @@ export function AdminTrackingLinksPage() { if (!canManage) { return (
- - + +
) } @@ -332,7 +332,7 @@ export function AdminTrackingLinksPage() {
diff --git a/frontend/src/features/admin/AdminWalletLedgerPage.tsx b/frontend/src/features/admin/AdminWalletLedgerPage.tsx index ec9eb99..a2ea5e5 100644 --- a/frontend/src/features/admin/AdminWalletLedgerPage.tsx +++ b/frontend/src/features/admin/AdminWalletLedgerPage.tsx @@ -125,7 +125,7 @@ export function AdminWalletLedgerPage() { if (!canView) { return (
- +
) @@ -135,7 +135,7 @@ export function AdminWalletLedgerPage() {
@@ -197,7 +197,7 @@ export function AdminWalletLedgerPage() { type="error" showIcon className="admin-table-alert" - message="钱包流水加载失败" + message="资金流水加载失败" description={errorText(ledgerQuery.error)} /> ) : null} @@ -214,7 +214,7 @@ export function AdminWalletLedgerPage() { setPage(nextPage) setPageSize(nextSize) }} - emptyDescription="还没有钱包流水" + emptyDescription="还没有资金流水" />
) diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts index 16f0036..1b4526d 100644 --- a/frontend/src/features/admin/api.ts +++ b/frontend/src/features/admin/api.ts @@ -169,6 +169,13 @@ export async function fetchAdminMe() { return data.data } +export async function updateAdminSupportStatus(status: 'online' | 'offline' | 'busy') { + const { data } = await adminClient.put< + ApiResponse<{ updated: boolean; support_status: string }> + >('/admin/me/support-status', { status }) + return data.data +} + export async function logoutAdmin() { await adminClient.post('/admin/auth/logout') } diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index 9cc5af0..ed64057 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -1,35 +1,58 @@ -import { useState, type ReactNode } from 'react' +import { useEffect, useState, type ReactNode } from 'react' +import { useQuery } from '@tanstack/react-query' import { + App, Avatar, Breadcrumb, Button, Dropdown, Layout, Menu, - Space, Tag, Typography, type MenuProps, } from 'antd' import { AccountBookOutlined, + ApartmentOutlined, + AppstoreOutlined, + BankOutlined, BellOutlined, + CloudServerOutlined, + ControlOutlined, + CreditCardOutlined, CustomerServiceOutlined, DashboardOutlined, FileDoneOutlined, + FileSearchOutlined, + FileTextOutlined, + FormOutlined, GiftOutlined, + LineChartOutlined, + LinkOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, + MessageOutlined, + MoneyCollectOutlined, + NotificationOutlined, + ProfileOutlined, + QrcodeOutlined, + RollbackOutlined, + SafetyCertificateOutlined, + SafetyOutlined, SettingOutlined, ShareAltOutlined, ShopOutlined, - ShoppingCartOutlined, + SoundOutlined, + SwapOutlined, TeamOutlined, + UserSwitchOutlined, } from '@ant-design/icons' import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom' import { BrandLogo } from '@/components/BrandLogo' +import { fetchAdminNotificationUnreadCount, updateAdminSupportStatus } from '@/features/admin/api' import { getAdminBasePath, getAdminLoginPath } from '@/shared/api/adminClient' import { useAdminSessionStore } from '@/stores/adminSession' @@ -38,6 +61,7 @@ const { Sider, Header, Content } = Layout interface NavItem { key: string label: string + icon?: ReactNode permission?: string } @@ -53,131 +77,270 @@ const allNavGroups: NavGroup[] = [ key: 'workspace', label: '工作台', icon: , - children: [{ key: '/', label: '仪表盘', permission: 'dashboard:view' }], - }, - { - key: 'trade', - label: '交易', - icon: , children: [ - { key: '/orders', label: '订单管理', permission: 'order:list' }, - { key: '/orders/refund-review', label: '退款审核', permission: 'order:close' }, - { key: '/pickup', label: '线下提号', permission: 'order:pickup' }, - { key: '/disputes', label: '申诉仲裁', permission: 'dispute:view' }, + { key: '/', label: '仪表盘', icon: , permission: 'dashboard:view' }, ], }, { - key: 'listings', - label: '商品', - icon: , + key: 'business', + label: '业务管理', + icon: , children: [ - { key: '/listings', label: '商品管理', permission: 'listing:view' }, - { key: '/listings/review', label: '商品审核', permission: 'listing:approve' }, - ], - }, - { - key: 'users', - label: '用户', - icon: , - children: [{ key: '/users', label: '用户管理', permission: 'user:view' }], - }, - { - key: 'finance', - label: '财务', - icon: , - children: [ - { key: '/finance', label: '财务看板', permission: 'wallet:view' }, - { key: '/finance/details', label: '资金明细', permission: 'wallet:view' }, - { key: '/finance/disbursements', label: '资金出款', permission: 'wallet:view' }, - { key: '/payments', label: '支付流水', permission: 'wallet:view' }, - { key: '/wallet-ledger', label: '钱包流水', permission: 'wallet:view' }, + { key: '/users', label: '用户管理', icon: , permission: 'user:view' }, { - key: '/finance/operating-expenses', - label: '运营开支', - permission: 'finance:operating_expense', + key: '/orders', + label: '订单管理', + icon: , + permission: 'order:list', + }, + { + key: '/orders/refund-review', + label: '退款审核', + icon: , + permission: 'order:close', + }, + { key: '/pickup', label: '线下提号', icon: , permission: 'order:pickup' }, + { + key: '/listings', + label: '商品管理', + icon: , + permission: 'listing:view', + }, + { + key: '/listings/review', + label: '商品审核', + icon: , + permission: 'listing:approve', + }, + { + key: '/crash/categories', + label: '撞车分类', + icon: , + permission: 'mohong:product_view', + }, + { + key: '/crash/products', + label: '撞车商品', + icon: , + permission: 'mohong:product_view', + }, + { + key: '/crash/orders', + label: '撞车订单', + icon: , + permission: 'mohong:order_view', + }, + { + key: '/crash/config', + label: '撞车配置', + icon: , + permission: 'mohong:config', }, - { key: '/withdrawals', label: '提现审核', permission: 'withdrawal:list' }, ], }, { key: 'distribution', - label: '分销', + label: '分销管理', icon: , children: [ - { key: '/distribution/dashboard', label: '分销看板', permission: 'distribution:site_view' }, - { key: '/distribution/sites', label: '站点管理', permission: 'distribution:site_view' }, - { key: '/distribution/orders', label: '分销订单', permission: 'distribution:order_view' }, + { + key: '/distribution/sites', + label: '分销站点管理', + icon: , + permission: 'distribution:site_view', + }, + { + key: '/distribution/qrcode-pool', + label: '分站二维码池', + icon: , + permission: 'qrcode:view', + }, + { + key: '/distribution/rules', + label: '分销规则', + icon: , + permission: 'distribution:rule_view', + }, + { + key: '/distribution/orders', + label: '分销订单', + icon: , + permission: 'distribution:order_view', + }, { key: '/distribution/settlements', - label: '结算批次', + label: '结算对账', + icon: , permission: 'distribution:settlement_view', }, - { key: '/distribution/users', label: '分销用户', permission: 'distribution:site_view' }, { - key: '/distribution/announcements', - label: '站点公告', - permission: 'distribution:announcement_view', + key: '/invitations', + label: '邀请返利管理', + icon: , + permission: 'distribution:site_view', }, - { key: '/distribution/listings', label: '分销商品', permission: 'distribution:account_view' }, + ], + }, + { + key: 'ad-tracking', + label: '推广追踪', + icon: , + children: [ { - key: '/distribution/payment-channels', - label: '支付通道', - permission: 'distribution:payment_view', + key: '/tracking-links', + label: '推广链接管理', + icon: , + permission: 'ad_tracking:manage', }, - { key: '/distribution/wallet', label: '站点钱包', permission: 'distribution:settlement_view' }, - { key: '/distribution/rules', label: '规则版本', permission: 'distribution:rule_view' }, - { key: '/distribution/qrcodes', label: '二维码池', permission: 'qrcode:view' }, - { key: '/distribution/sms', label: '站点短信', permission: 'distribution:site_view' }, - { key: '/distribution/invitation', label: '邀请返利', permission: 'distribution:site_view' }, ], }, { - key: 'crash', - label: '摸红', - icon: , - children: [ - { key: '/crash/categories', label: '分类管理', permission: 'mohong:product_view' }, - { key: '/crash/products', label: '商品管理', permission: 'mohong:product_view' }, - { key: '/crash/orders', label: '订单管理', permission: 'mohong:order_view' }, - { key: '/crash/config', label: '摸红配置', permission: 'mohong:config' }, - ], - }, - { - key: 'operations', - label: '运营', - icon: , - children: [ - { key: '/announcements', label: '公告管理', permission: 'announcement:view' }, - { key: '/tracking-links', label: '推广追踪', permission: 'ad_tracking:manage' }, - { key: '/notifications', label: '通知中心', permission: 'notification:view' }, - { key: '/push', label: '推送通知', permission: 'system_config:view' }, - { key: '/invitations', label: '邀请管理', permission: 'distribution:site_view' }, - ], - }, - { - key: 'support', - label: '客服', + key: 'support-risk', + label: '客服与风控', icon: , children: [ - { key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' }, - { key: '/chats', label: '客服会话', permission: 'chat:view' }, - { key: '/qrcode-pool', label: '二维码池', permission: 'qrcode:view' }, - { key: '/manual-entries', label: '手工录单', permission: 'cs_manual_entry:view' }, - { key: '/chat-quick-replies', label: '快捷回复', permission: 'chat:view' }, - { key: '/chat-welcome', label: '欢迎语配置', permission: 'system_config:view' }, + { + key: '/disputes', + label: '仲裁中心', + icon: , + permission: 'dispute:view', + }, + { key: '/chats', label: '客服群聊', icon: , permission: 'chat:view' }, + { + key: '/qrcode-pool', + label: '二维码池', + icon: , + permission: 'qrcode:view', + }, + { + key: '/support-groups', + label: '客服分组', + icon: , + permission: 'chat:manage', + }, + { + key: '/manual-entries', + label: '手动录单', + icon: , + permission: 'cs_manual_entry:view', + }, ], }, { - key: 'system', - label: '系统', + key: 'finance', + label: '财务管理', + icon: , + children: [ + { + key: '/finance/dashboard', + label: '财务仪表盘', + icon: , + permission: 'wallet:view', + }, + { + key: '/finance/details', + label: '财务明细', + icon: , + permission: 'wallet:view', + }, + { + key: '/finance/disbursements', + label: '资金出款', + icon: , + permission: 'wallet:view', + }, + { + key: '/finance/operating-expenses', + label: '运营开支', + icon: , + permission: 'finance:operating_expense', + }, + { + key: '/wallet-ledger', + label: '资金流水', + icon: , + permission: 'wallet:view', + }, + { + key: '/payments', + label: '支付流水', + icon: , + permission: 'wallet:view', + }, + { + key: '/withdrawals', + label: '提现审核', + icon: , + permission: 'withdrawal:list', + }, + { + key: '/payment-configs', + label: '支付配置', + icon: , + permission: 'payment_config:list', + }, + ], + }, + { + key: 'content-config', + label: '内容与配置', icon: , children: [ - { key: '/system-configs', label: '系统配置', permission: 'system_config:view' }, - { key: '/roles', label: '角色权限', permission: 'role:manage' }, - { key: '/admin-users', label: '管理员账号', permission: 'admin_user:manage' }, - { key: '/payment-configs', label: '支付配置', permission: 'payment_config:list' }, - { key: '/backups', label: '备份管理', permission: 'backup:view' }, - { key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' }, + { + key: '/announcements', + label: '公告管理', + icon: , + permission: 'announcement:view', + }, + { + key: '/notifications', + label: '通知中心', + icon: , + permission: 'notification:view', + }, + { + key: '/push', + label: '推送通知', + icon: , + permission: 'system_config:view', + }, + { + key: '/system-configs', + label: '系统配置', + icon: , + permission: 'system_config:view', + }, + { + key: '/backups', + label: '数据备份', + icon: , + permission: 'backup:view', + }, + ], + }, + { + key: 'system-auth', + label: '系统权限', + icon: , + children: [ + { + key: '/audit-logs', + label: '审计日志', + icon: , + permission: 'audit_log:view', + }, + { + key: '/admin-users', + label: '管理员管理', + icon: , + permission: 'admin_user:manage', + }, + { + key: '/roles', + label: '角色管理', + icon: , + permission: 'role:manage', + }, ], }, ] @@ -186,35 +349,37 @@ const sectionMeta: Record = { '/orders': { title: '订单管理', description: '查看全量订单、交接记录与结账状态。' }, '/orders/refund-review': { title: '退款审核', description: '审核退款申请并核对资金影响。' }, '/pickup': { title: '线下提号', description: '管理员线下提号,站内结算或线下打款确认。' }, - '/disputes': { title: '申诉仲裁', description: '处理订单争议、证据与仲裁结果。' }, + '/disputes': { title: '仲裁中心', description: '处理订单争议、证据与仲裁结果。' }, '/listings': { title: '商品管理', description: '查看商品、账号资产与上下架状态。' }, '/listings/review': { title: '商品审核', description: '审核待上架商品与账号截图。' }, '/users': { title: '用户管理', description: '查看用户、实名状态与交易记录。' }, - '/finance': { title: '财务看板', description: '查看收入、退款、结算与资金趋势。' }, - '/finance/details': { title: '资金明细', description: '按订单核对实付、退款、号主结算与平台净额。' }, + '/finance': { title: '财务仪表盘', description: '查看收入、退款、结算与资金趋势。' }, + '/finance/dashboard': { title: '财务仪表盘', description: '查看收入、退款、结算与资金趋势。' }, + '/finance/details': { title: '财务明细', description: '按订单核对实付、退款、号主结算与平台净额。' }, '/payments': { title: '支付流水', description: '核对渠道支付单、回调状态与失败原因。' }, - '/wallet-ledger': { title: '钱包流水', description: '按用户、订单与业务类型核对余额变动。' }, + '/wallet-ledger': { title: '资金流水', description: '按用户、订单与业务类型核对余额变动。' }, '/finance/disbursements': { title: '资金出款', description: '查看代管出款、提现与线下出款明细。' }, '/finance/operating-expenses': { title: '运营开支', description: '登记与核对日常运营支出。' }, '/withdrawals': { title: '提现审核', description: '审核提现申请与打款确认。' }, - '/distribution/sites': { title: '分销站点', description: '管理分站、规则、结算与站点钱包。' }, + '/distribution/sites': { title: '分销站点管理', description: '管理分站、规则、结算与站点钱包。' }, '/distribution/dashboard': { title: '分销看板', description: '查看站点收号、订单、利润与近 7 天趋势。' }, '/distribution/orders': { title: '分销订单', description: '核对分销订单快照与冷却期释放状态。' }, - '/distribution/settlements': { title: '结算批次', description: '生成人工批次并核对、释放站内份额。' }, + '/distribution/settlements': { title: '结算对账', description: '生成人工批次并核对、释放站内份额。' }, '/distribution/users': { title: '分销用户', description: '查看站点注册客户、下单客户与消费贡献。' }, '/distribution/announcements': { title: '站点公告', description: '维护分站自己的公告中心与弹窗公告。' }, '/distribution/listings': { title: '分销商品', description: '查看收货商品并控制账号展示与加价。' }, '/distribution/payment-channels': { title: '支付通道', description: '为分站绑定主站支付商户配置。' }, '/distribution/wallet': { title: '站点钱包', description: '查看分站余额、结算进度与提现记录。' }, - '/distribution/rules': { title: '规则版本', description: '维护全局与站点级分账规则版本。' }, - '/distribution/qrcodes': { title: '二维码池', description: '按站点维护发布群二维码池与发放状态。' }, + '/distribution/rules': { title: '分销规则', description: '维护全局与站点级分账规则版本。' }, + '/distribution/qrcodes': { title: '分站二维码池', description: '按站点维护发布群二维码池与发放状态。' }, + '/distribution/qrcode-pool': { title: '分站二维码池', description: '按站点维护发布群二维码池与发放状态。' }, '/distribution/sms': { title: '站点短信', description: '为分站配置独立短信签名、模板与密钥。' }, '/distribution/invitation': { title: '邀请返利', description: '配置分站邀请好友的返利比例。' }, '/announcements': { title: '公告管理', description: '维护主站与分站公告。' }, - '/roles': { title: '角色权限', description: '维护后台角色并分配资源级操作权限。' }, - '/admin-users': { title: '管理员账号', description: '维护后台账号、启用状态与角色绑定。' }, + '/roles': { title: '角色管理', description: '维护后台角色并分配资源级操作权限。' }, + '/admin-users': { title: '管理员管理', description: '维护后台账号、启用状态与角色绑定。' }, '/manual-entries': { - title: '手工录单', + title: '手动录单', description: '录入系统外成单编号,用于对账与客服绩效统计。', }, '/chat-quick-replies': { @@ -222,25 +387,29 @@ const sectionMeta: Record = { description: '维护客服会话常用话术与全局可见范围。', }, '/tracking-links': { - title: '推广追踪', + title: '推广链接管理', description: '管理推广短链、渠道成本与注册下单转化。', }, - '/crash/categories': { title: '摸红分类', description: '维护撞车商城商品分类与排序。' }, - '/crash/products': { title: '摸红商品', description: '维护撞车商城商品、价格与固定二维码。' }, - '/crash/orders': { title: '摸红订单', description: '处理撞车订单的接单、完成、取消与退款。' }, - '/crash/config': { title: '摸红配置', description: '维护默认二维码与订单一键复制模板。' }, + '/crash/categories': { title: '撞车分类', description: '维护撞车商城商品分类与排序。' }, + '/crash/products': { title: '撞车商品', description: '维护撞车商城商品、价格与固定二维码。' }, + '/crash/orders': { title: '撞车订单', description: '处理撞车订单的接单、完成、取消与退款。' }, + '/crash/config': { title: '撞车配置', description: '维护默认二维码与订单一键复制模板。' }, '/notifications': { title: '通知中心', description: '查看后台系统通知并批量标记已读。' }, '/push': { title: '推送通知', description: '维护推送渠道与通知规则。' }, '/invitations': { - title: '邀请管理', + title: '邀请返利管理', description: '维护邀请返利比例,查看邀请关系与返利流水。', }, '/chat-support-groups': { - title: '客服群组', + title: '客服分组', + description: '维护客服群与坐席分配,支撑会话分流。', + }, + '/support-groups': { + title: '客服分组', description: '维护客服群与坐席分配,支撑会话分流。', }, '/chats': { - title: '客服会话', + title: '客服群聊', description: '按阶段筛选会话、回复用户并转接客服。', }, '/chat-welcome': { @@ -253,22 +422,55 @@ const sectionMeta: Record = { }, '/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, '/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' }, - '/backups': { title: '备份管理', description: '查看备份健康状态并维护定时备份计划。' }, + '/backups': { title: '数据备份', description: '查看备份健康状态并维护定时备份计划。' }, '/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' }, } +const MENU_ALIASES: Record = { + '/finance': '/finance/dashboard', + '/chat-support-groups': '/support-groups', + '/distribution/qrcodes': '/distribution/qrcode-pool', +} + +const SUPPORT_STATUS_LABELS: Record = { + online: '在线', + busy: '忙碌', + offline: '离线', +} + export function AdminLayout() { const navigate = useNavigate() const location = useLocation() const admin = useAdminSessionStore(state => state.admin) const logout = useAdminSessionStore(state => state.logout) + const { message } = App.useApp() const [collapsed, setCollapsed] = useState(false) + const [openKeys, setOpenKeys] = useState([]) + const [supportStatus, setSupportStatus] = useState<'online' | 'offline' | 'busy'>( + admin?.support_status ?? 'offline' + ) + + useEffect(() => { + if (admin?.support_status) setSupportStatus(admin.support_status) + }, [admin?.support_status]) + + const canViewNotifications = Boolean( + admin?.permissions?.includes('*') || admin?.permissions?.includes('notification:view') + ) + + const { data: unreadCount = 0 } = useQuery({ + queryKey: ['admin-notification-unread'], + queryFn: fetchAdminNotificationUnreadCount, + enabled: canViewNotifications, + refetchInterval: 60_000, + }) const basePath = getAdminBasePath() const relativePath = location.pathname.startsWith(basePath) && location.pathname !== basePath ? location.pathname.slice(basePath.length) : '/' + const selectedKey = MENU_ALIASES[relativePath] ?? relativePath const can = (permission?: string) => { if (!permission) return true @@ -290,19 +492,42 @@ export function AdminLayout() { children: group.children.map(item => ({ key: item.key, label: item.label, + icon: item.icon, })), })) + const activeGroupKey = visibleGroups.find(group => + group.children.some(item => item.key === selectedKey) + )?.key + + useEffect(() => { + if (activeGroupKey) setOpenKeys([activeGroupKey]) + }, [activeGroupKey]) + const currentLabel = - visibleGroups.flatMap(group => group.children).find(item => item.key === relativePath)?.label || - sectionMeta[relativePath]?.title || - (relativePath === '/' ? '仪表盘' : '功能建设中') + visibleGroups.flatMap(group => group.children).find(item => item.key === selectedKey)?.label || + sectionMeta[selectedKey]?.title || + (selectedKey === '/' ? '仪表盘' : '功能建设中') const onLogout = async () => { await logout() navigate(getAdminLoginPath(), { replace: true }) } + const adminName = admin?.nickname || admin?.username || '管理员' + const adminRoleText = admin?.roles?.[0]?.name || '管理员' + + const handleStatusChange = async (status: 'online' | 'offline' | 'busy') => { + if (status === supportStatus) return + try { + await updateAdminSupportStatus(status) + setSupportStatus(status) + message.success(`已切换为${SUPPORT_STATUS_LABELS[status]}`) + } catch { + message.error('状态更新失败') + } + } + return ( group.key)} + selectedKeys={[selectedKey]} + openKeys={openKeys} + onOpenChange={keys => { + const opened = keys.find(key => !openKeys.includes(key)) + setOpenKeys(opened ? [opened] : []) + }} onClick={({ key }) => { if (key.startsWith('/')) { navigate(key === '/' ? basePath : `${basePath}${key}`) @@ -335,8 +564,59 @@ export function AdminLayout() { {!collapsed ? (
- 解忧商行 · 运营后台 - Phase 2 · 交易与商品 + {canViewNotifications ? ( + + + 通知中心 + {unreadCount > 0 ? ( + {unreadCount > 99 ? '99+' : unreadCount} + ) : null} + + ) : null} + + + void handleStatusChange(key as 'online' | 'offline' | 'busy'), + }} + > + + + + , onClick: onLogout }, + ], + }} + > + +
) : null}
@@ -358,31 +638,6 @@ export function AdminLayout() { ]} />
- - - {admin?.roles?.[0] ? {admin.roles[0].name} : null} - , onClick: onLogout }, - ], - }} - placement="bottomRight" - > - - - diff --git a/frontend/src/styles/admin-shell.css b/frontend/src/styles/admin-shell.css index d6b203e..2b025f8 100644 --- a/frontend/src/styles/admin-shell.css +++ b/frontend/src/styles/admin-shell.css @@ -66,11 +66,101 @@ display: flex; flex-direction: column; gap: 4px; - padding: 16px 20px; + padding: 14px 16px 16px; border-top: 1px solid var(--line); font-size: 11px; } +.admin-sider__notice { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 8px; + border-radius: 6px; + color: var(--ink-secondary); + font-size: 13px; +} + +.admin-sider__notice:hover { + background: #f1ece0; + color: var(--brand-ink); +} + +.admin-sider__notice .ant-tag { + margin-inline-end: 0; + margin-left: auto; +} + +.admin-sider__status { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 7px 8px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--ink-secondary); + font-size: 13px; + text-align: left; + cursor: pointer; +} + +.admin-sider__status:hover { + background: #f1ece0; +} + +.admin-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #9ca3af; +} + +.admin-status-dot.is-online { + background: #10b981; +} + +.admin-status-dot.is-busy { + background: #f59e0b; +} + +.admin-status-dot.is-offline { + background: #9ca3af; +} + +.admin-sider__account { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + margin-top: 4px; + padding: 10px 8px 2px; + border: 0; + border-top: 1px solid var(--line); + background: transparent; + text-align: left; + cursor: pointer; +} + +.admin-sider__account-text { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; +} + +.admin-sider__account-text strong { + color: var(--brand-ink); + font-size: 13px; + font-weight: 600; +} + +.admin-sider__account-text small { + color: var(--ink-muted); + font-size: 11px; +} + .admin-main { min-width: 0; background: var(--paper);