对接知识库、租户管理API,侧边栏加入用户信息和退出登录
This commit is contained in:
@@ -1,98 +1,56 @@
|
||||
import { useState } from 'react'
|
||||
import { Tree, Table, Input, Button, Modal, Form, Select, Tag, Space, Empty, Progress, Popconfirm } from 'antd'
|
||||
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
|
||||
interface Category {
|
||||
key: string
|
||||
title: string
|
||||
children?: Category[]
|
||||
}
|
||||
|
||||
interface KnowledgeEntry {
|
||||
id: string
|
||||
title: string
|
||||
category: string
|
||||
content: string
|
||||
status: 'published' | 'draft'
|
||||
usageCount: number
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const categories: Category[] = [
|
||||
{ key: 'product', title: '产品常见问题', children: [
|
||||
{ key: 'product-feature', title: '功能介绍' },
|
||||
{ key: 'product-compare', title: '版本对比' },
|
||||
]},
|
||||
{ key: 'after-sale', title: '售后服务', children: [
|
||||
{ key: 'refund', title: '退换货政策' },
|
||||
{ key: 'warranty', title: '保修说明' },
|
||||
]},
|
||||
{ key: 'support', title: '技术支持' },
|
||||
{ key: 'policy', title: '政策条款' },
|
||||
{ key: 'quick-reply', title: '快捷回复模板' },
|
||||
]
|
||||
|
||||
const mockEntries: KnowledgeEntry[] = [
|
||||
{ id: '1', title: '如何修改登录密码', category: 'product', content: '登录后在右上角头像→个人设置→修改密码中输入旧密码和新密码即可完成修改。', status: 'published', usageCount: 156, updatedAt: '2026-07-14' },
|
||||
{ id: '2', title: '支持哪些支付方式', category: 'product-feature', content: '目前支持微信支付、支付宝、银行转账三种方式。企业客户支持对公转账。', status: 'published', usageCount: 98, updatedAt: '2026-07-13' },
|
||||
{ id: '3', title: '免费版和专业版区别', category: 'product-compare', content: '免费版提供基础的客服功能,专业版支持更多渠道接入、高级报表和API接口。', status: 'published', usageCount: 72, updatedAt: '2026-07-12' },
|
||||
{ id: '4', title: '退货流程说明', category: 'refund', content: '在订单页面点击申请退货→填写退货原因→等待审核→寄回商品→退款到账,全程约3-5个工作日。', status: 'published', usageCount: 45, updatedAt: '2026-07-11' },
|
||||
{ id: '5', title: 'API 接口文档', category: 'support', content: '开发者文档请访问 docs.example.com/api,提供 REST API 和 Webhook 两种接入方式。', status: 'draft', usageCount: 0, updatedAt: '2026-07-10' },
|
||||
{ id: '6', title: '欢迎语模板', category: 'quick-reply', content: '您好!欢迎来到客服云,请问有什么可以帮您的?', status: 'published', usageCount: 230, updatedAt: '2026-07-09' },
|
||||
{ id: '7', title: '结束语模板', category: 'quick-reply', content: '感谢您的咨询,如有其他问题随时联系我们,祝您生活愉快!', status: 'published', usageCount: 189, updatedAt: '2026-07-09' },
|
||||
]
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Tree, Table, Input, Button, Modal, Form, Select, Tag, Empty, Progress } from 'antd'
|
||||
import { SearchOutlined, PlusOutlined, EditOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
import { getKnowledgeCategories, getKnowledgeEntries, type KnowledgeEntry } from '@/services/api'
|
||||
|
||||
const Knowledge = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('product')
|
||||
const [categories, setCategories] = useState<{ key: string; title: string; id: number }[]>([])
|
||||
const [entries, setEntries] = useState<KnowledgeEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('')
|
||||
const [search, setSearch] = useState('')
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingEntry, setEditingEntry] = useState<KnowledgeEntry | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const filtered = mockEntries.filter(e => {
|
||||
if (selectedCategory && e.category !== selectedCategory) return false
|
||||
if (search && !e.title.includes(search) && !e.content.includes(search)) return false
|
||||
return true
|
||||
})
|
||||
useEffect(() => {
|
||||
loadCategories()
|
||||
}, [])
|
||||
|
||||
const openNew = () => {
|
||||
setEditingEntry(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({ category: selectedCategory, status: 'draft' })
|
||||
setModalOpen(true)
|
||||
useEffect(() => {
|
||||
loadEntries()
|
||||
}, [selectedCategory, search, page])
|
||||
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const res = await getKnowledgeCategories()
|
||||
setCategories((res.data as any[]).map((c: any) => ({ key: String(c.id), title: c.name, id: c.id })))
|
||||
} catch { /* fallback */ }
|
||||
}
|
||||
|
||||
const openEdit = (entry: KnowledgeEntry) => {
|
||||
setEditingEntry(entry)
|
||||
form.setFieldsValue(entry)
|
||||
setModalOpen(true)
|
||||
const loadEntries = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getKnowledgeEntries({ category_id: selectedCategory, search, page })
|
||||
setEntries(res.list)
|
||||
setTotal(res.total)
|
||||
} catch { setEntries([]) } finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', render: (text: string) => <span className="text-sm font-medium text-neutral-800">{text}</span> },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80, render: (status: string) => (
|
||||
<Tag color={status === 'published' ? 'green' : 'default'}>{status === 'published' ? '已发布' : '草稿'}</Tag>
|
||||
)},
|
||||
{ title: '使用频率', dataIndex: 'usageCount', key: 'usageCount', width: 200, render: (count: number) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress percent={Math.min(count / 2, 100)} size="small" showInfo={false} strokeColor="#2563eb" className="flex-1 max-w-32" />
|
||||
<span className="text-xs text-neutral-400">{count}次</span>
|
||||
</div>
|
||||
)},
|
||||
{ title: '更新时间', dataIndex: 'updatedAt', key: 'updatedAt', width: 120, render: (text: string) => <span className="text-xs text-neutral-400">{text}</span> },
|
||||
{ title: '操作', key: 'actions', width: 120, render: (_: unknown, record: KnowledgeEntry) => (
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={(e) => { e.stopPropagation(); openEdit(record) }}>编辑</Button>
|
||||
<Popconfirm title="确定删除?" onConfirm={(e) => e?.stopPropagation()} onCancel={(e) => e?.stopPropagation()}>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={(e) => e.stopPropagation()} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', render: (t: string) => <span className="text-sm font-medium text-neutral-800">{t}</span> },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80, render: (s: string) => <Tag color={s === 'published' ? 'green' : 'default'}>{s === 'published' ? '已发布' : '草稿'}</Tag> },
|
||||
{ title: '使用频率', dataIndex: 'usage_count', key: 'usage_count', width: 200, render: (c: number) => (
|
||||
<div className="flex items-center gap-2"><Progress percent={Math.min(c / 2, 100)} size="small" showInfo={false} strokeColor="#2563eb" className="flex-1 max-w-32" /><span className="text-xs text-neutral-400">{c}次</span></div>
|
||||
)},
|
||||
{ title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 120, render: (t: string) => <span className="text-xs text-neutral-400">{t ? new Date(t).toLocaleDateString() : '-'}</span> },
|
||||
{ title: '操作', key: 'actions', width: 100, render: () => <Button type="link" size="small" icon={<EditOutlined />}>编辑</Button> },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
{/* 左侧分类树 */}
|
||||
<div className="w-[240px] flex-shrink-0 bg-white border-r border-neutral-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FileTextOutlined className="text-blue-500" />
|
||||
@@ -101,54 +59,30 @@ const Knowledge = () => {
|
||||
<Tree
|
||||
treeData={categories as any}
|
||||
defaultExpandAll
|
||||
selectedKeys={[selectedCategory]}
|
||||
onSelect={keys => { if (keys.length > 0) setSelectedCategory(keys[0] as string) }}
|
||||
selectedKeys={selectedCategory ? [selectedCategory] : []}
|
||||
onSelect={keys => setSelectedCategory(keys.length > 0 ? (keys[0] as string) : '')}
|
||||
blockNode
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
<div className="flex-1 flex flex-col p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-neutral-800">知识库</h2>
|
||||
<Input prefix={<SearchOutlined />} placeholder="搜索标题或内容..." value={search} onChange={e => setSearch(e.target.value)} className="w-56" size="small" allowClear />
|
||||
<Input prefix={<SearchOutlined />} placeholder="搜索..." value={search} onChange={e => { setSearch(e.target.value); setPage(1) }} className="w-48" size="small" allowClear />
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openNew}>新建条目</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingEntry(null); form.resetFields(); setModalOpen(true) }}>新建条目</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-white rounded-lg border border-neutral-200 overflow-hidden">
|
||||
<Table
|
||||
dataSource={filtered}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
pagination={{ pageSize: 10 }}
|
||||
locale={{ emptyText: <Empty description="暂无知识条目" /> }}
|
||||
/>
|
||||
<Table dataSource={entries} columns={columns} rowKey="id" size="middle" loading={loading}
|
||||
pagination={{ current: page, total, pageSize: 10, showTotal: t => `共 ${t} 条`, onChange: p => setPage(p) }}
|
||||
locale={{ emptyText: <Empty description="暂无知识条目" /> }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={editingEntry ? '编辑知识条目' : '新建知识条目'}
|
||||
open={modalOpen}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
width={640}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={_values => setModalOpen(false)} className="mt-4">
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="条目标题,2-100字符" maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
|
||||
<Select options={categories.map(c => ({ value: c.key, label: c.title }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}>
|
||||
<Input.TextArea rows={6} placeholder="条目内容,支持纯文本" maxLength={5000} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select options={[{ value: 'published', label: '发布' }, { value: 'draft', label: '草稿' }]} />
|
||||
</Form.Item>
|
||||
<Modal title={editingEntry ? '编辑' : '新建'} open={modalOpen} onCancel={() => setModalOpen(false)} onOk={() => form.submit()} width={640}>
|
||||
<Form form={form} layout="vertical" onFinish={() => setModalOpen(false)} className="mt-4">
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}><Input maxLength={100} /></Form.Item>
|
||||
<Form.Item name="content" label="内容" rules={[{ required: true }]}><Input.TextArea rows={5} maxLength={5000} showCount /></Form.Item>
|
||||
<Form.Item name="status" label="状态"><Select options={[{ value: 'published', label: '发布' }, { value: 'draft', label: '草稿' }]} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user