Skip to content

Commit

Permalink
feat: new feature knowledge
Browse files Browse the repository at this point in the history
  • Loading branch information
fantasticit committed Mar 6, 2021
1 parent fb6058c commit ea0b92f
Show file tree
Hide file tree
Showing 56 changed files with 2,863 additions and 573 deletions.
2 changes: 2 additions & 0 deletions packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"antd": "^3.26.7",
"array-move": "^3.0.1",
"axios": "^0.19.2",
"classnames": "^2.2.6",
"date-fns": "^2.17.0",
Expand All @@ -27,6 +28,7 @@
"react": "16.12.0",
"react-dom": "16.12.0",
"react-helmet": "^5.2.1",
"react-sortable-hoc": "^1.11.0",
"showdown": "^1.9.1",
"viewerjs": "^1.5.0"
},
Expand Down
21 changes: 11 additions & 10 deletions packages/admin/pages/comment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ const Comment: NextPage<IProps> = ({ comments: defaultComments = [], total = 0 }
}, [params]);

const columns = [
{
title: '状态',
dataIndex: 'pass',
key: 'pass',
fixed: 'left',
width: 100,
render: (_, record) => <CommentStatus comment={record} />,
},
{
title: '称呼',
dataIndex: 'name',
key: 'name',
fixed: 'left',
},
{
title: '联系方式',
Expand Down Expand Up @@ -67,20 +74,14 @@ const Comment: NextPage<IProps> = ({ comments: defaultComments = [], total = 0 }
},
{
title: '管理文章',
dataIndex: 'hostId',
key: 'hostId',
dataIndex: 'url',
key: 'url',
width: 100,
render: (_, record) => {
return <CommentArticle comment={record} />;
},
},
{
title: '状态',
dataIndex: 'pass',
key: 'pass',
width: 100,
render: (_, record) => <CommentStatus comment={record} />,
},

{
title: '创建时间',
dataIndex: 'createAt',
Expand Down
7 changes: 6 additions & 1 deletion packages/admin/pages/file/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,12 @@ const File: NextPage<IFileProps> = ({ files: defaultFiles = [], total }) => {
>
<Meta
title={file.originalname}
description={'上传于 ' + <LocaleTime date={file.createAt} />}
description={
<>
上传于
<LocaleTime date={file.createAt} />
</>
}
/>
</Card>
</List.Item>
Expand Down
225 changes: 225 additions & 0 deletions packages/admin/pages/knowledge/editor/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import React, { useCallback, useState } from 'react';
import { NextPage } from 'next';
import Router from 'next/router';
import cls from 'classnames';
import { Avatar, Divider, Icon, Input, Button, Popconfirm, Popover, message } from 'antd';
import { SortableHandle, SortableContainer, SortableElement } from 'react-sortable-hoc';
import arrayMove from 'array-move';
import { KnowledgeProvider } from '@/providers/knowledge';
import { AdminLayout } from '@/layout/AdminLayout';
import { Editor } from '@/components/Editor';
import { FileSelectDrawer } from '@/components/FileSelectDrawer';
import { useForceUpdate } from '@/hooks/useForceUpdate';
import { useToggle } from '@/hooks/useToggle';
import styles from './index.module.scss';

const DragHandle = SortableHandle(() => <span>::</span>);

interface IProps {
id: string | number;
knowledge: Partial<IKnowledge>;
}

const Page: NextPage<IProps> = ({ id, knowledge }) => {
const forceUpdate = useForceUpdate();
const [loading, setLoading] = useState(false);
const [popVisible, togglePopVisible] = useToggle(false);
const [fileVisible, toggleFileVisible] = useToggle(false);
const [newTitle, setNewTitle] = useState('');
const [currentIndex, setCurrentIndex] = useState(-1);
const [chapters, setChapters] = useState<Array<Partial<IKnowledge>>>(knowledge.children || []);
const currentChapter = chapters[currentIndex] || null;

const SortableItem = SortableElement(({ value: idx }) => (
<li
key={idx}
className={cls({ [styles.active]: idx === currentIndex, [styles.item]: true })}
onClick={() => setCurrentIndex(idx)}
>
<DragHandle />
<span>{chapters[idx].title}</span>
<Popconfirm
title="确认删除?"
onConfirm={() => deleteKnowledge(idx)}
okText="确定"
cancelText="取消"
>
<Icon type="delete" onClick={(e) => e.stopPropagation()} />
</Popconfirm>
</li>
));

const SortableList = SortableContainer(({ items }) => {
return (
<ul className={styles.menu}>
{items.map((_, index) => (
<SortableItem key={`item-${index}`} index={index} value={index} />
))}
</ul>
);
});

const onSortEnd = useCallback(({ oldIndex, newIndex }) => {
setChapters((chapters) => {
return arrayMove(chapters, oldIndex, newIndex);
});
}, []);

const createNewKnowledge = useCallback(() => {
const title = newTitle.trim();
if (!title) return;
setChapters((chapters) => {
chapters.push({
title: title,
content: '',
});
return chapters;
});
setCurrentIndex(chapters.length - 1);
setNewTitle('');
togglePopVisible();
forceUpdate();
}, [newTitle]);

const deleteKnowledge = useCallback(
(idx) => {
const handle = () => {
setChapters((chapters) => {
chapters.splice(idx, 1);
return chapters;
});
forceUpdate();
setCurrentIndex(currentIndex - 1);
};
const target = chapters[idx];
if (target.id) {
KnowledgeProvider.deleteKnowledge(target.id).then(() => {
handle();
message.success('已保存');
});
} else {
handle();
}
},
[chapters.length, currentIndex]
);

const patchKnowledge = useCallback(
(patch) => {
if (currentIndex < 0) return;
setChapters((chapters) => {
const target = chapters[currentIndex];
if (!target) return chapters;
target.content = patch.value;
target.html = patch.html;
target.toc = patch.toc;
return chapters;
});
},
[currentIndex, chapters.length]
);

const save = useCallback(() => {
if (!chapters || !chapters.length) return;
chapters.forEach((chapter, idx) => {
chapter.order = idx;
});
setLoading(true);
const promises = chapters.map((chapter) => {
if (chapter.parentId) {
return KnowledgeProvider.updateKnowledge(chapter.id, chapter);
} else {
return KnowledgeProvider.createChapters([{ ...chapter, parentId: id }]);
}
});
Promise.all(promises as Array<Promise<IKnowledge>>).then((res) => {
const data = res.flat(Infinity);
setChapters(data);
forceUpdate();
setLoading(false);
});
}, [id, chapters]);

return (
<AdminLayout onlyAside>
<div className={styles.wrap}>
<aside className={styles.aside}>
<header>
<div>
<Avatar shape="square" size="large" src={knowledge.cover} />
<span style={{ marginLeft: 8 }}>{knowledge.title}</span>
</div>
<Icon type="close" onClick={() => Router.push('/knowledge')} />
</header>
<Divider type="horizontal" />
<main>
{chapters.length > 0 ? (
<div className={cls(styles.action, styles.saveAction)}>
<span>{chapters.length}篇文章</span>
<Button size="small" type="primary" onClick={save} loading={loading}>
保存
</Button>
</div>
) : null}
<Popover
content={
<div style={{ display: 'flex' }}>
<Input
autoFocus
width={240}
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
<Button style={{ marginLeft: 8 }} type="primary" onClick={createNewKnowledge}>
新建
</Button>
</div>
}
visible={popVisible}
onVisibleChange={togglePopVisible}
placement="rightTop"
trigger="click"
>
<div className={styles.action}>
<span>新建</span>
<Icon type="plus" />
</div>
</Popover>
<div className={styles.action} onClick={toggleFileVisible}>
<span>文件</span>
<Icon type="folder" />
</div>
<FileSelectDrawer isCopy visible={fileVisible} onClose={toggleFileVisible} />
</main>
<Divider type="horizontal" />
<footer>
{/* <ul>
{chapters.map((_, idx) => {
return <SortableItem key={`item-${idx}`} index={idx} value={idx} />;
})}
</ul> */}
<SortableList items={chapters} onSortEnd={onSortEnd} useDragHandle />
</footer>
</aside>
<main className={styles.main}>
{currentChapter ? (
<Editor
defaultValue={(currentChapter && currentChapter.content) || ''}
onChange={patchKnowledge}
/>
) : (
<div className={styles.helper}>请新建章节(或者选择章节进行编辑)</div>
)}
</main>
</div>
</AdminLayout>
);
};

Page.getInitialProps = async (ctx) => {
const { id } = ctx.query;
const knowledge = await KnowledgeProvider.getKnowledge(id);
return { id, knowledge } as { id: string | number; knowledge: IKnowledge };
};

export default Page;

0 comments on commit ea0b92f

Please sign in to comment.