feat: 新增知识卡片页面及路由支持

- 添加知识卡片 API 接口与 Mock 数据
- 实现知识卡片列表展示与筛选功能
- 实现知识卡片详情抽屉
- 支持路由嵌套配置与菜单展开
This commit is contained in:
2026-05-27 19:46:05 +08:00
parent 9575e5898f
commit 34e52ab1d0
8 changed files with 808 additions and 12 deletions

View File

@@ -1 +1 @@
2048d3c140545556010ceb3eea341153 2f1b47a26399ec7c847d08cf81f5dc77

View File

@@ -0,0 +1,38 @@
import { get } from '@/utils/request'
export type KnowledgeCardType = 'global' | 'product' | 'chat'
export type KnowledgeCardAuditStatus = 'pending' | 'approved'
export interface RelatedProduct {
id: number
name: string
}
export interface RelatedChat {
id: number
name: string
}
export interface KnowledgeCard {
id: number
title: string
content: string
type: KnowledgeCardType
auditStatus: KnowledgeCardAuditStatus
author: string
updatedAt: string
coverColor: string
tags: string[]
relatedProducts: RelatedProduct[]
relatedChats: RelatedChat[]
}
export interface GetKnowledgeCardsParams {
type?: KnowledgeCardType
auditStatus?: KnowledgeCardAuditStatus
}
/** 获取知识卡片列表 */
export function getKnowledgeCards(params?: GetKnowledgeCardsParams) {
return get<KnowledgeCard[]>('/knowledge/cards', params as Record<string, string>)
}

View File

@@ -1,13 +1,25 @@
import { Layout, Menu, Button, Popconfirm, Avatar, theme } from 'antd' import { Layout, Menu, Button, Popconfirm, Avatar, theme } from 'antd'
import { MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons' import { MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons'
import { useNavigate, useLocation, Outlet } from 'react-router-dom' import { useNavigate, useLocation, Outlet } from 'react-router-dom'
import { routes } from '@/router/routes' import { routes, type RouteConfig } from '@/router/routes'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import type { MenuProps } from 'antd' import type { MenuProps } from 'antd'
const { Sider, Header, Content } = Layout const { Sider, Header, Content } = Layout
// 递归扁平化所有路由,用于查找当前路径对应的标题
function flatRouteLabel(list: RouteConfig[], pathname: string): string {
for (const r of list) {
if (r.path === pathname) return r.label
if (r.children) {
const found = flatRouteLabel(r.children, pathname)
if (found) return found
}
}
return ''
}
const SIDER_WIDTH = 220 const SIDER_WIDTH = 220
const SIDER_COLLAPSED_WIDTH = 64 const SIDER_COLLAPSED_WIDTH = 64
const HEADER_HEIGHT = 64 const HEADER_HEIGHT = 64
@@ -102,6 +114,7 @@ function BasicLayout() {
<Menu <Menu
mode="inline" mode="inline"
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
defaultOpenKeys={routes.filter(r => r.children).map(r => r.path)}
items={menuItems} items={menuItems}
onClick={handleMenuClick} onClick={handleMenuClick}
style={{ borderInlineEnd: 'none', paddingTop: 8 }} style={{ borderInlineEnd: 'none', paddingTop: 8 }}
@@ -143,7 +156,7 @@ function BasicLayout() {
{/* 当前页面标题 */} {/* 当前页面标题 */}
<span style={{ fontSize: 16, fontWeight: 600, color: token.colorText }}> <span style={{ fontSize: 16, fontWeight: 600, color: token.colorText }}>
{routes.find(r => r.path === location.pathname)?.label ?? ''} {flatRouteLabel(routes, location.pathname)}
</span> </span>
{/* 右侧操作区 */} {/* 右侧操作区 */}

View File

@@ -1,6 +1,8 @@
import { userHandlers } from './handlers/user' import { userHandlers } from './handlers/user'
import { knowledgeCardsHandlers } from './handlers/knowledgeCards'
/** 所有 mock handlers按模块聚合 */ /** 所有 mock handlers按模块聚合 */
export const handlers = [ export const handlers = [
...userHandlers, ...userHandlers,
...knowledgeCardsHandlers,
] ]

View File

@@ -0,0 +1,158 @@
import { http, HttpResponse, delay } from 'msw'
import type { KnowledgeCard } from '@/api/knowledgeCards'
const mockCards: KnowledgeCard[] = [
{
id: 1,
title: '如何处理客户退款申请',
content:
'当客户提出退款申请时首先需要核实订单信息确认是否在退款政策范围内。若符合条件按照标准流程发起退款并在24小时内完成审核。对于特殊情况需上报主管审批。',
type: 'global',
auditStatus: 'approved',
author: '张三',
updatedAt: '2025-05-20',
coverColor: '#e6f4ff',
tags: ['退款', '售后', '客服流程'],
relatedProducts: [{ id: 101, name: '夏季连衣裙' }, { id: 102, name: '运动鞋' }],
relatedChats: [{ id: 201, name: '退款标准话术' }],
},
{
id: 2,
title: '新品发布常见问题解答',
content:
'本文档汇总了新品发布期间客户最常咨询的问题,包括发货时间、库存查询、预售规则等,客服可直接引用回复客户。',
type: 'product',
auditStatus: 'pending',
author: '李四',
updatedAt: '2025-05-22',
coverColor: '#fff7e6',
tags: ['新品', 'FAQ', '预售'],
relatedProducts: [{ id: 103, name: '2025夏季新款T恤' }],
relatedChats: [{ id: 202, name: '新品咨询话术' }, { id: 203, name: '预售说明' }],
},
{
id: 3,
title: '节假日活动话术模板',
content:
'双十一、春节、618等大促节点的标准欢迎语、活动介绍话术以及常见异议处理方式。使用时请根据当前活动情况替换活动名称和折扣信息。',
type: 'chat',
auditStatus: 'approved',
author: '王五',
updatedAt: '2025-05-18',
coverColor: '#f6ffed',
tags: ['大促', '话术', '双十一', '618'],
relatedProducts: [],
relatedChats: [{ id: 204, name: '双十一欢迎语' }, { id: 205, name: '618活动说明' }],
},
{
id: 4,
title: '商品质量问题处理规范',
content:
'针对客户反馈商品质量问题的完整处理流程:收集问题描述与凭证、判断责任归属、协商解决方案(补寄/退款/换货)、记录归档。',
type: 'product',
auditStatus: 'approved',
author: '赵六',
updatedAt: '2025-05-15',
coverColor: '#fff0f6',
tags: ['质量问题', '换货', '售后'],
relatedProducts: [{ id: 104, name: '皮质背包' }, { id: 105, name: '陶瓷餐具套装' }],
relatedChats: [{ id: 206, name: '质量投诉处理话术' }],
},
{
id: 5,
title: '客户服务礼貌用语规范',
content:
'统一客服沟通风格,包括:开场白、道歉用语、感谢用语、告别语等,禁止使用的不当用语列表,以及如何在情绪激动的客户面前保持专业态度。',
type: 'chat',
auditStatus: 'pending',
author: '孙七',
updatedAt: '2025-05-23',
coverColor: '#e6f4ff',
tags: ['礼貌用语', '规范', '沟通技巧'],
relatedProducts: [],
relatedChats: [{ id: 207, name: '通用开场白' }, { id: 208, name: '道歉话术' }],
},
{
id: 6,
title: '快递异常处理指南',
content:
'物流延误、快递丢失、破损到货等场景的处理方式。包括如何查询快递信息、与快递公司协商、赔偿标准,以及如何安抚客户情绪。',
type: 'global',
auditStatus: 'approved',
author: '周八',
updatedAt: '2025-05-10',
coverColor: '#f9f0ff',
tags: ['物流', '快递异常', '赔偿'],
relatedProducts: [],
relatedChats: [{ id: 209, name: '物流异常安抚话术' }],
},
{
id: 7,
title: '会员积分兑换规则说明',
content:
'平台会员积分的获取方式(消费、签到、邀请好友)、兑换比例、有效期及使用限制。常见积分问题的排查和处理方式。',
type: 'global',
auditStatus: 'pending',
author: '吴九',
updatedAt: '2025-05-21',
coverColor: '#fff7e6',
tags: ['会员', '积分', '兑换'],
relatedProducts: [],
relatedChats: [{ id: 210, name: '积分查询话术' }],
},
{
id: 8,
title: '爆款商品卖点话术',
content:
'本季度热销TOP10商品的核心卖点提炼、差异化优势描述和场景化推荐话术帮助客服快速向客户介绍商品价值并促成购买决策。',
type: 'product',
auditStatus: 'approved',
author: '郑十',
updatedAt: '2025-05-17',
coverColor: '#f6ffed',
tags: ['爆款', '卖点', '推荐话术'],
relatedProducts: [
{ id: 106, name: '无线蓝牙耳机' },
{ id: 107, name: '智能手表' },
{ id: 108, name: '颈部按摩仪' },
],
relatedChats: [{ id: 211, name: '爆款推荐话术' }, { id: 212, name: '商品对比话术' }],
},
{
id: 9,
title: '投诉升级处理流程',
content:
'当客户投诉进入升级状态(如威胁投诉平台、媒体曝光)时的处置预案:评估风险等级、上报节点、授权范围,以及事后复盘和改进记录要求。',
type: 'global',
auditStatus: 'pending',
author: '张三',
updatedAt: '2025-05-24',
coverColor: '#fff0f6',
tags: ['投诉', '升级处理', '风险'],
relatedProducts: [],
relatedChats: [{ id: 213, name: '升级投诉安抚话术' }],
},
]
export const knowledgeCardsHandlers = [
http.get('/knowledge/cards', async ({ request }) => {
await delay(300)
const url = new URL(request.url)
const type = url.searchParams.get('type')
const auditStatus = url.searchParams.get('auditStatus')
const filtered = mockCards.filter(card => {
const typeMatch = !type || card.type === type
const auditMatch = !auditStatus || card.auditStatus === auditStatus
return typeMatch && auditMatch
})
return HttpResponse.json({
code: '0',
msg: 'ok',
time: Date.now(),
ok: true,
data: filtered,
})
}),
]

View File

@@ -0,0 +1,563 @@
import { useState, useEffect } from 'react'
import {
Card,
Tag,
Typography,
Space,
Segmented,
Flex,
Avatar,
theme,
Drawer,
Divider,
Spin,
Tooltip,
message,
} from 'antd'
import {
CheckCircleOutlined,
ClockCircleOutlined,
GlobalOutlined,
ShoppingOutlined,
MessageOutlined,
CalendarOutlined,
UserOutlined,
CopyOutlined,
ShoppingCartOutlined,
CommentOutlined,
} from '@ant-design/icons'
import {
getKnowledgeCards,
type KnowledgeCard,
type KnowledgeCardType,
type KnowledgeCardAuditStatus,
} from '@/api/knowledgeCards'
const { Text, Paragraph, Title } = Typography
type AuditFilter = 'all' | KnowledgeCardAuditStatus
type TypeFilter = 'all' | KnowledgeCardType
const typeConfig = {
global: { label: '全域知识', icon: <GlobalOutlined />, color: 'blue' },
product: { label: '商品知识', icon: <ShoppingOutlined />, color: 'orange' },
chat: { label: '聊天知识', icon: <MessageOutlined />, color: 'green' },
}
const auditConfig = {
pending: { label: '未审核', icon: <ClockCircleOutlined />, color: 'warning' as const },
approved: { label: '已审核', icon: <CheckCircleOutlined />, color: 'success' as const },
}
// 卡片固定尺寸
const CARD_WIDTH = 280
const CARD_HEIGHT = 220
function KnowledgeCardItem({
card,
onClick,
}: {
card: KnowledgeCard
onClick: (card: KnowledgeCard) => void
}) {
const { token } = theme.useToken()
const type = typeConfig[card.type]
const audit = auditConfig[card.auditStatus]
return (
<Card
hoverable
onClick={() => onClick(card)}
style={{
borderRadius: token.borderRadiusLG,
overflow: 'hidden',
width: CARD_WIDTH,
height: CARD_HEIGHT,
cursor: 'pointer',
flexShrink: 0,
}}
styles={{ body: { padding: 0, height: '100%' } }}
>
{/* 顶部色块装饰 */}
<div
style={{
height: 6,
background:
card.type === 'global'
? token.colorPrimary
: card.type === 'product'
? token.colorWarning
: token.colorSuccess,
flexShrink: 0,
}}
/>
<div
style={{
padding: token.paddingMD,
height: `calc(100% - 6px)`,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxSizing: 'border-box',
}}
>
{/* 标题最多1行超出省略 */}
<Text
strong
ellipsis
style={{
fontSize: token.fontSizeLG,
display: 'block',
marginBottom: token.marginXS,
color: token.colorText,
flexShrink: 0,
}}
>
{card.title}
</Text>
{/* 正文:固定行数,超出省略,不可展开 */}
<div
style={{
flex: 1,
overflow: 'hidden',
marginBottom: token.marginSM,
}}
>
<Paragraph
type="secondary"
ellipsis={{ rows: 3, expandable: false }}
style={{
marginBottom: 0,
fontSize: token.fontSize,
}}
>
{card.content}
</Paragraph>
</div>
{/* 底部作者信息 */}
<Flex align="center" justify="space-between" style={{ flexShrink: 0, marginBottom: 6 }}>
<Space size={4}>
<Avatar
size={20}
style={{
backgroundColor: token.colorPrimary,
fontSize: 11,
}}
>
{card.author[0]}
</Avatar>
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
{card.author} · {card.updatedAt}
</Text>
</Space>
</Flex>
{/* 标签行 */}
<Flex gap={6} style={{ flexShrink: 0 }} wrap>
<Tag
icon={audit.icon}
color={audit.color}
style={{ margin: 0, borderRadius: token.borderRadiusSM }}
>
{audit.label}
</Tag>
<Tag
icon={type.icon}
color={type.color}
style={{ margin: 0, borderRadius: token.borderRadiusSM }}
>
{type.label}
</Tag>
</Flex>
</div>
</Card>
)
}
function KnowledgeCardDetail({
card,
open,
onClose,
}: {
card: KnowledgeCard | null
open: boolean
onClose: () => void
}) {
const { token } = theme.useToken()
const [messageApi, contextHolder] = message.useMessage()
if (!card) return null
const type = typeConfig[card.type]
const audit = auditConfig[card.auditStatus]
const topBarColor =
card.type === 'global'
? token.colorPrimary
: card.type === 'product'
? token.colorWarning
: token.colorSuccess
const handleCopyId = () => {
navigator.clipboard.writeText(String(card.id)).then(() => {
messageApi.success('ID 已复制')
})
}
const sidebarBg = token.colorFillQuaternary
return (
<Drawer
title={null}
placement="right"
width={760}
open={open}
onClose={onClose}
styles={{
header: { display: 'none' },
body: { padding: 0, display: 'flex', flexDirection: 'column' },
}}
>
{contextHolder}
{/* 顶部色块 */}
<div style={{ height: 6, background: topBarColor, flexShrink: 0 }} />
{/* 左右两栏 */}
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* 左侧:主内容 */}
<div
style={{
flex: 1,
overflow: 'auto',
padding: token.paddingLG,
borderRight: `1px solid ${token.colorBorderSecondary}`,
}}
>
{/* 标题区 */}
<Title level={4} style={{ marginTop: 0, marginBottom: token.marginSM }}>
{card.title}
</Title>
{/* 状态标签 */}
<Flex gap={8} style={{ marginBottom: token.marginMD }}>
<Tag
icon={audit.icon}
color={audit.color}
style={{ borderRadius: token.borderRadiusSM }}
>
{audit.label}
</Tag>
<Tag
icon={type.icon}
color={type.color}
style={{ borderRadius: token.borderRadiusSM }}
>
{type.label}
</Tag>
</Flex>
<Divider style={{ marginTop: 0, marginBottom: token.marginMD }} />
{/* 元信息 */}
<Flex gap={token.marginLG} style={{ marginBottom: token.marginLG }}>
<Space size={6}>
<UserOutlined style={{ color: token.colorTextSecondary }} />
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
{card.author}
</Text>
</Space>
<Space size={6}>
<CalendarOutlined style={{ color: token.colorTextSecondary }} />
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
{card.updatedAt}
</Text>
</Space>
</Flex>
{/* 正文完整内容 */}
<Paragraph
style={{
fontSize: token.fontSize,
lineHeight: 1.8,
color: token.colorText,
whiteSpace: 'pre-wrap',
}}
>
{card.content}
</Paragraph>
</div>
{/* 右侧:属性面板 */}
<div
style={{
width: 220,
flexShrink: 0,
overflow: 'auto',
background: sidebarBg,
display: 'flex',
flexDirection: 'column',
gap: 0,
}}
>
{/* 基础信息 */}
<div style={{ padding: `${token.paddingMD}px ${token.paddingMD}px ${token.paddingSM}px` }}>
<Text
type="secondary"
style={{
fontSize: token.fontSizeSM,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'block',
marginBottom: token.marginSM,
}}
>
</Text>
{/* ID 可复制 */}
<div style={{ marginBottom: token.marginSM }}>
<Text type="secondary" style={{ fontSize: token.fontSizeSM, display: 'block', marginBottom: 4 }}>
ID
</Text>
<Flex
align="center"
gap={6}
style={{
background: token.colorBgContainer,
border: `1px solid ${token.colorBorder}`,
borderRadius: token.borderRadiusSM,
padding: `4px ${token.paddingSM}px`,
cursor: 'pointer',
}}
onClick={handleCopyId}
>
<Text style={{ fontSize: token.fontSizeSM, flex: 1, fontFamily: 'monospace' }}>
{card.id}
</Text>
<Tooltip title="复制 ID">
<CopyOutlined style={{ fontSize: 12, color: token.colorTextSecondary }} />
</Tooltip>
</Flex>
</div>
{/* 多选标签 */}
<div>
<Text type="secondary" style={{ fontSize: token.fontSizeSM, display: 'block', marginBottom: 4 }}>
</Text>
{card.tags.length > 0 ? (
<Flex gap={4} wrap>
{card.tags.map(tag => (
<Tag
key={tag}
style={{
margin: 0,
borderRadius: token.borderRadiusSM,
fontSize: token.fontSizeSM,
}}
>
{tag}
</Tag>
))}
</Flex>
) : (
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
</Text>
)}
</div>
</div>
<Divider style={{ margin: `${token.marginXS}px 0` }} />
{/* 扩展信息 */}
<div style={{ padding: `${token.paddingSM}px ${token.paddingMD}px ${token.paddingMD}px` }}>
<Text
type="secondary"
style={{
fontSize: token.fontSizeSM,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'block',
marginBottom: token.marginSM,
}}
>
</Text>
{/* 关联商品 */}
<div style={{ marginBottom: token.marginMD }}>
<Space size={4} style={{ marginBottom: 6 }}>
<ShoppingCartOutlined style={{ fontSize: token.fontSizeSM, color: token.colorTextSecondary }} />
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
</Text>
</Space>
{card.relatedProducts.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{card.relatedProducts.map(p => (
<div
key={p.id}
style={{
background: token.colorBgContainer,
border: `1px solid ${token.colorBorder}`,
borderRadius: token.borderRadiusSM,
padding: `4px ${token.paddingSM}px`,
}}
>
<Text style={{ fontSize: token.fontSizeSM }} ellipsis>
{p.name}
</Text>
</div>
))}
</div>
) : (
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
</Text>
)}
</div>
{/* 关联聊天 */}
<div>
<Space size={4} style={{ marginBottom: 6 }}>
<CommentOutlined style={{ fontSize: token.fontSizeSM, color: token.colorTextSecondary }} />
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
</Text>
</Space>
{card.relatedChats.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{card.relatedChats.map(c => (
<div
key={c.id}
style={{
background: token.colorBgContainer,
border: `1px solid ${token.colorBorder}`,
borderRadius: token.borderRadiusSM,
padding: `4px ${token.paddingSM}px`,
}}
>
<Text style={{ fontSize: token.fontSizeSM }} ellipsis>
{c.name}
</Text>
</div>
))}
</div>
) : (
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
</Text>
)}
</div>
</div>
</div>
</div>
</Drawer>
)
}
function KnowledgeCards() {
const { token } = theme.useToken()
const [auditFilter, setAuditFilter] = useState<AuditFilter>('all')
const [typeFilter, setTypeFilter] = useState<TypeFilter>('all')
const [cards, setCards] = useState<KnowledgeCard[]>([])
const [loading, setLoading] = useState(false)
const [selectedCard, setSelectedCard] = useState<KnowledgeCard | null>(null)
const [drawerOpen, setDrawerOpen] = useState(false)
useEffect(() => {
const fetchCards = async () => {
setLoading(true)
const params: Record<string, string> = {}
if (auditFilter !== 'all') params.auditStatus = auditFilter
if (typeFilter !== 'all') params.type = typeFilter
const res = await getKnowledgeCards(
params as Parameters<typeof getKnowledgeCards>[0],
)
if (res.ok && res.data) {
setCards(res.data)
}
setLoading(false)
}
fetchCards()
}, [auditFilter, typeFilter])
const handleCardClick = (card: KnowledgeCard) => {
setSelectedCard(card)
setDrawerOpen(true)
}
const handleDrawerClose = () => {
setDrawerOpen(false)
}
return (
<div style={{ padding: token.paddingLG }}>
{/* 筛选栏 */}
<Flex gap={token.marginMD} wrap style={{ marginBottom: token.marginLG }}>
<Segmented
value={auditFilter}
onChange={v => setAuditFilter(v as AuditFilter)}
options={[
{ label: '全部', value: 'all' },
{ label: '未审核', value: 'pending' },
{ label: '已审核', value: 'approved' },
]}
/>
<Segmented
value={typeFilter}
onChange={v => setTypeFilter(v as TypeFilter)}
options={[
{ label: '全部', value: 'all' },
{ label: '全域知识', value: 'global' },
{ label: '商品知识', value: 'product' },
{ label: '聊天知识', value: 'chat' },
]}
/>
</Flex>
{/* 固定大小卡片网格 */}
<Spin spinning={loading}>
{cards.length > 0 ? (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: token.marginMD,
}}
>
{cards.map(card => (
<KnowledgeCardItem key={card.id} card={card} onClick={handleCardClick} />
))}
</div>
) : (
!loading && (
<Flex
justify="center"
align="center"
style={{ height: 300, color: token.colorTextQuaternary, fontSize: token.fontSizeLG }}
>
</Flex>
)
)}
</Spin>
{/* 详情抽屉 */}
<KnowledgeCardDetail
card={selectedCard}
open={drawerOpen}
onClose={handleDrawerClose}
/>
</div>
)
}
export default KnowledgeCards

View File

@@ -1,17 +1,24 @@
import type { ReactNode } from 'react'
import { createBrowserRouter } from 'react-router-dom' import { createBrowserRouter } from 'react-router-dom'
import BasicLayout from '@/layouts/BasicLayout' import BasicLayout from '@/layouts/BasicLayout'
import LoginPage from '@/pages/login' import LoginPage from '@/pages/login'
import NotFound from '@/pages/notFound' import NotFound from '@/pages/notFound'
import { AuthGuard, GuestGuard } from './AuthGuard' import { AuthGuard, GuestGuard } from './AuthGuard'
import { routes } from './routes' import { routes, type RouteConfig } from './routes'
const layoutChildren = routes // 递归展开所有路由(包含子路由),扁平化注册到 layoutChildren
.filter(r => r.path !== '*') function flattenRoutes(list: RouteConfig[]): { path: string; element: ReactNode }[] {
.map(({ path, element }) => ({ return list.flatMap(r => {
path, if (r.path === '*') return []
element, const self = r.path === '/'
...(path === '/' ? { index: true, path: undefined } : {}), ? [{ path: r.path, element: r.element, index: true }]
})) : [{ path: r.path, element: r.element }]
const children = r.children ? flattenRoutes(r.children) : []
return [...self, ...children] as { path: string; element: ReactNode }[]
})
}
const layoutChildren = flattenRoutes(routes)
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {

View File

@@ -1,7 +1,8 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { HomeOutlined } from '@ant-design/icons' import { HomeOutlined, BookOutlined, FileTextOutlined } from '@ant-design/icons'
import Home from '@/pages/home' import Home from '@/pages/home'
import NotFound from '@/pages/notFound' import NotFound from '@/pages/notFound'
import KnowledgeCards from '@/pages/knowledgeBase/cards'
export interface RouteConfig { export interface RouteConfig {
/** 路由路径 */ /** 路由路径 */
@@ -25,6 +26,20 @@ export const routes: RouteConfig[] = [
icon: <HomeOutlined />, icon: <HomeOutlined />,
element: <Home />, element: <Home />,
}, },
{
path: '/knowledge-base',
label: '知识库',
icon: <BookOutlined />,
element: <></>,
children: [
{
path: '/knowledge-base/cards',
label: '知识卡片',
icon: <FileTextOutlined />,
element: <KnowledgeCards />,
},
],
},
{ {
path: '*', path: '*',
label: '页面不存在', label: '页面不存在',