mirror of https://github.com/chaitin/PandaWiki.git
feat: contribute
This commit is contained in:
parent
8af576080d
commit
5a64e3f021
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"search": "搜索",
|
||||
"search_no_results_1": "哦不!",
|
||||
"search_no_results_2": "没有找到相关表情",
|
||||
"pick": "选择一个表情…",
|
||||
"add_custom": "添加自定义表情",
|
||||
"categories": {
|
||||
"activity": "活动",
|
||||
"custom": "自定义",
|
||||
"flags": "旗帜",
|
||||
"foods": "食物与饮品",
|
||||
"frequent": "最近使用",
|
||||
"nature": "动物与自然",
|
||||
"objects": "物品",
|
||||
"people": "表情与角色",
|
||||
"places": "旅行与景点",
|
||||
"search": "搜索结果",
|
||||
"symbols": "符号"
|
||||
},
|
||||
"skins": {
|
||||
"choose": "选择默认肤色",
|
||||
"1": "默认",
|
||||
"2": "白色",
|
||||
"3": "偏白",
|
||||
"4": "中等",
|
||||
"5": "偏黑",
|
||||
"6": "黑色"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -3,7 +3,7 @@ import Picker from '@emoji-mart/react';
|
|||
import { Box, IconButton, Popover, SxProps } from '@mui/material';
|
||||
import { Icon } from '@ctzhian/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
import zh from '../../../public/emoji-data/zh.json';
|
||||
import zh from '../../assets/emoji-data/zh.json';
|
||||
|
||||
interface EmojiPickerProps {
|
||||
type: 1 | 2;
|
||||
|
|
@ -39,7 +39,7 @@ const EmojiPicker: React.FC<EmojiPickerProps> = ({
|
|||
};
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(emoji: any) => {
|
||||
(emoji: { native: string }) => {
|
||||
onChange?.(emoji.native);
|
||||
handleClose();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const OtherBread = {
|
|||
feedback: { title: '反馈', to: '/feedback' },
|
||||
application: { title: '设置', to: '/setting' },
|
||||
release: { title: '发布', to: '/release' },
|
||||
contribution: { title: '贡献', to: '/contribution' },
|
||||
};
|
||||
|
||||
const Bread = () => {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ const MENUS = [
|
|||
ConstsUserKBPermission.UserKBPermissionDataOperate,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '贡献',
|
||||
value: '/contribution',
|
||||
pathname: 'contribution',
|
||||
icon: 'icon-gongxian',
|
||||
show: true,
|
||||
perms: [ConstsUserKBPermission.UserKBPermissionFullControl],
|
||||
},
|
||||
{
|
||||
label: '问答',
|
||||
value: '/conversation',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
import { Box, Button, Stack, Typography, Divider } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import { Modal } from '@ctzhian/ui';
|
||||
import type { GithubComChaitinPandaWikiProApiContributeV1ContributeItem } from '@/request/pro/types';
|
||||
import {
|
||||
ConstsContributeStatus,
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp,
|
||||
ConstsContributeType,
|
||||
} from '@/request/pro/types';
|
||||
import { getApiProV1ContributeDetail } from '@/request/pro/Contribute';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { EditorDiff, Editor, useTiptap } from '@ctzhian/tiptap';
|
||||
import { IconWenjian } from '@panda-wiki/icons';
|
||||
|
||||
type ContributePreviewModalProps = {
|
||||
open: boolean;
|
||||
row: GithubComChaitinPandaWikiProApiContributeV1ContributeItem | null;
|
||||
onClose: () => void;
|
||||
onAccept: () => void;
|
||||
onReject: () => void;
|
||||
};
|
||||
|
||||
export default function ContributePreviewModal(
|
||||
props: ContributePreviewModalProps,
|
||||
) {
|
||||
const [activeTab, setActiveTab] = useState('diff');
|
||||
const { open, row, onClose, onAccept, onReject } = props;
|
||||
const { kb_id = '' } = useAppSelector(state => state.config);
|
||||
const [data, setData] =
|
||||
useState<GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const editorRef = useTiptap({
|
||||
content: '',
|
||||
editable: false,
|
||||
immediatelyRender: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open && row) {
|
||||
getApiProV1ContributeDetail({ id: row.id!, kb_id }).then(res => {
|
||||
setData(res);
|
||||
});
|
||||
}
|
||||
}, [open, row, kb_id]);
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
if (value === 'content') {
|
||||
editorRef.editor.commands.setContent(data?.content || '');
|
||||
} else if (value === 'old_content') {
|
||||
editorRef.editor.commands.setContent(data?.original_node?.content || '');
|
||||
} else if (value === 'diff') {
|
||||
editorRef.editor.commands.setContent('');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
handleTabChange('diff');
|
||||
setData(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={'1200px'}
|
||||
sx={{
|
||||
'.MuiDialogContent-root': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}}
|
||||
title={
|
||||
<Stack direction='row' alignItems='center' gap={2}>
|
||||
<Box>
|
||||
来自 {row?.auth_name || '匿名用户'} 的
|
||||
{row?.type === ConstsContributeType.ContributeTypeAdd
|
||||
? '新增'
|
||||
: '修改'}
|
||||
</Box>
|
||||
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
|
||||
{dayjs(row?.created_at).fromNow()}
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
footer={null}
|
||||
>
|
||||
<Stack direction='row' sx={{ overflow: 'hidden', height: '100%' }}>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{
|
||||
overflow: 'auto',
|
||||
flex: 1,
|
||||
pr: 2,
|
||||
borderRight: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction='row'
|
||||
gap={1}
|
||||
sx={{ bgcolor: 'background.paper2', p: 1, borderRadius: '10px' }}
|
||||
>
|
||||
<Box sx={{ fontSize: 14, fontWeight: 'bold', flexShrink: 0 }}>
|
||||
提交说明:
|
||||
</Box>
|
||||
|
||||
<Box sx={{ fontSize: 14, color: 'text.tertiary' }}>
|
||||
{data?.reason || '-'}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
gap={1}
|
||||
sx={{ fontSize: 24, fontWeight: 700, pb: 2 }}
|
||||
>
|
||||
<IconWenjian /> {row?.node_name || '-'}
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ display: activeTab === 'diff' ? 'none' : 'block' }}>
|
||||
<Editor editor={editorRef.editor} />
|
||||
</Box>
|
||||
|
||||
{(data?.content || data?.original_node?.content) &&
|
||||
activeTab === 'diff' && (
|
||||
<EditorDiff
|
||||
oldHtml={data?.original_node?.content || ''}
|
||||
newHtml={data?.content || ''}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack justifyContent='space-between' sx={{ width: 220, pl: 2 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: 16, fontWeight: 600, pb: 2 }}>
|
||||
对比
|
||||
</Typography>
|
||||
<Stack gap={2}>
|
||||
<Button
|
||||
size='large'
|
||||
variant={activeTab === 'diff' ? 'contained' : 'outlined'}
|
||||
fullWidth
|
||||
onClick={() => handleTabChange('diff')}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
borderRadius: '10px',
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
<Stack alignItems='flex-start' spacing={0.25}>
|
||||
<Box>差异对比</Box>
|
||||
<Typography variant='caption' sx={{ opacity: 0.8 }}>
|
||||
直观查看变更点
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='large'
|
||||
variant={activeTab === 'content' ? 'contained' : 'outlined'}
|
||||
fullWidth
|
||||
onClick={() => handleTabChange('content')}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
borderRadius: '10px',
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
<Stack alignItems='flex-start' spacing={0.25}>
|
||||
<Box>修改后</Box>
|
||||
<Typography variant='caption' sx={{ opacity: 0.8 }}>
|
||||
当前候选内容
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='large'
|
||||
variant={activeTab === 'old_content' ? 'contained' : 'outlined'}
|
||||
fullWidth
|
||||
onClick={() => handleTabChange('old_content')}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
borderRadius: '10px',
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
<Stack alignItems='flex-start' spacing={0.25}>
|
||||
<Box>修改前</Box>
|
||||
<Typography variant='caption' sx={{ opacity: 0.8 }}>
|
||||
原始文档内容
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Divider sx={{ my: 3 }} />
|
||||
<Stack direction='row' gap={1} justifyContent='flex-end'>
|
||||
{row?.status ===
|
||||
ConstsContributeStatus.ContributeStatusPending ? (
|
||||
<>
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
color='error'
|
||||
onClick={onReject}
|
||||
>
|
||||
拒绝
|
||||
</Button>
|
||||
<Button size='small' variant='contained' onClick={onAccept}>
|
||||
采纳
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={onClose} size='small' variant='contained'>
|
||||
关闭
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { ITreeItem } from '@/api';
|
||||
import Card from '@/components/Card';
|
||||
import DragTree from '@/components/Drag/DragTree';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { convertToTree } from '@/utils/drag';
|
||||
import { Box, Checkbox, Stack } from '@mui/material';
|
||||
import { Icon, Modal } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getApiV1NodeList } from '@/request/Node';
|
||||
|
||||
interface DocDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOk: (id: string) => void;
|
||||
}
|
||||
|
||||
const DocModal = ({ open, onClose, onOk }: DocDeleteProps) => {
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [tree, setTree] = useState<ITreeItem[]>([]);
|
||||
const [folderIds, setFolderIds] = useState<string[]>([]);
|
||||
|
||||
const handleOk = () => {
|
||||
onOk(folderIds.includes('root') ? '' : folderIds[0]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
getApiV1NodeList({ kb_id }).then(res => {
|
||||
const folder = res.filter(it => it.type === 1);
|
||||
setTree(convertToTree(folder));
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Modal title='选择目录' open={open} onCancel={onClose} onOk={handleOk}>
|
||||
<Card sx={{ bgcolor: 'background.paper3', p: 1 }}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{ fontSize: 14, cursor: 'pointer' }}
|
||||
>
|
||||
<Checkbox
|
||||
sx={{ color: 'text.disabled', width: '35px', height: '35px' }}
|
||||
checked={folderIds.includes('root')}
|
||||
onChange={() => {
|
||||
setFolderIds(folderIds.includes('root') ? [] : ['root']);
|
||||
}}
|
||||
/>
|
||||
<Icon type={'icon-wenjianjia-kai'} />
|
||||
<Box>根路径</Box>
|
||||
</Stack>
|
||||
<DragTree
|
||||
ui='select'
|
||||
selected={folderIds}
|
||||
data={tree}
|
||||
readOnly={true}
|
||||
relativeSelect={false}
|
||||
onSelectChange={(ids, id = '') => {
|
||||
if (folderIds.includes(id)) {
|
||||
setFolderIds([]);
|
||||
} else {
|
||||
setFolderIds([id]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocModal;
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import { Box, Chip, Stack, TextField } from '@mui/material';
|
||||
import Card from '@/components/Card';
|
||||
import { tableSx } from '@/constant/styles';
|
||||
import dayjs from 'dayjs';
|
||||
import { Table, Ellipsis, message, Modal } from '@ctzhian/ui';
|
||||
import type { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||
import DocModal from './DocModal';
|
||||
|
||||
import {
|
||||
getApiProV1ContributeList,
|
||||
postApiProV1ContributeAudit,
|
||||
} from '@/request/pro/Contribute';
|
||||
import {
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeItem,
|
||||
ConstsContributeStatus,
|
||||
ConstsContributeType,
|
||||
} from '@/request/pro/types';
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import { useAppSelector } from '@/store';
|
||||
import ContributePreviewModal from './ContributePreviewModal';
|
||||
|
||||
const StyledSearchRow = styled(Stack)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
gap: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}));
|
||||
|
||||
const statusColorMap = {
|
||||
[ConstsContributeStatus.ContributeStatusApproved]: {
|
||||
label: '已采纳',
|
||||
color: 'success',
|
||||
},
|
||||
[ConstsContributeStatus.ContributeStatusRejected]: {
|
||||
label: '已拒绝',
|
||||
color: 'error',
|
||||
},
|
||||
[ConstsContributeStatus.ContributeStatusPending]: {
|
||||
label: '等待处理',
|
||||
color: 'warning',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default function ContributionPage() {
|
||||
const { kb_id = '', kbDetail } = useAppSelector(state => state.config);
|
||||
const [searchParams, setSearchParams] = useURLSearchParams();
|
||||
const page = Number(searchParams.get('page') || '1');
|
||||
const pageSize = Number(searchParams.get('page_size') || '20');
|
||||
const nodeNameParam = searchParams.get('node_name') || '';
|
||||
const authNameParam = searchParams.get('auth_name') || '';
|
||||
const [searchDoc, setSearchDoc] = useState(nodeNameParam);
|
||||
const [searchUser, setSearchUser] = useState(authNameParam);
|
||||
const [data, setData] = useState<
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeItem[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const [docModalOpen, setDocModalOpen] = useState(false);
|
||||
|
||||
const [previewRow, setPreviewRow] =
|
||||
useState<GithubComChaitinPandaWikiProApiContributeV1ContributeItem | null>(
|
||||
null,
|
||||
);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false);
|
||||
setPreviewRow(null);
|
||||
};
|
||||
|
||||
const handleDocModalOk = (id: string) => {
|
||||
setDocModalOpen(false);
|
||||
setPreviewRow(null);
|
||||
postApiProV1ContributeAudit({
|
||||
id: previewRow!.id!,
|
||||
kb_id,
|
||||
parent_id: id,
|
||||
status: 'approved',
|
||||
}).then(() => {
|
||||
getData();
|
||||
closeDialog();
|
||||
message.success('采纳成功');
|
||||
});
|
||||
};
|
||||
|
||||
const handleAccept = () => {
|
||||
if (previewRow?.type === ConstsContributeType.ContributeTypeAdd) {
|
||||
setDocModalOpen(true);
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: '采纳',
|
||||
content: '确定要采纳该修改吗?',
|
||||
okText: '采纳',
|
||||
onOk: () => {
|
||||
postApiProV1ContributeAudit({
|
||||
id: previewRow!.id!,
|
||||
kb_id,
|
||||
status: 'approved',
|
||||
}).then(() => {
|
||||
getData();
|
||||
closeDialog();
|
||||
message.success('采纳成功');
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleReject = () => {
|
||||
Modal.confirm({
|
||||
title: '拒绝',
|
||||
content: '确定要拒绝该修改吗?',
|
||||
okText: '拒绝',
|
||||
onOk: () => {
|
||||
postApiProV1ContributeAudit({
|
||||
id: previewRow!.id!,
|
||||
kb_id,
|
||||
status: 'rejected',
|
||||
}).then(() => {
|
||||
getData();
|
||||
closeDialog();
|
||||
message.success('拒绝成功');
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnType<GithubComChaitinPandaWikiProApiContributeV1ContributeItem>[] =
|
||||
[
|
||||
{
|
||||
dataIndex: 'node_name',
|
||||
title: '文档',
|
||||
width: 280,
|
||||
render: (text: string, record) => {
|
||||
return (
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
{record.type === ConstsContributeType.ContributeTypeAdd && (
|
||||
<Box
|
||||
sx={{
|
||||
transform: 'scale(0.8)',
|
||||
fontSize: 12,
|
||||
color: 'light.main',
|
||||
px: 0.5,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'error.main',
|
||||
}}
|
||||
>
|
||||
new
|
||||
</Box>
|
||||
)}
|
||||
<Ellipsis
|
||||
className='primary-color'
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setPreviewRow(record);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{text || record.node_name || ''}
|
||||
</Ellipsis>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'reason',
|
||||
title: '更新说明',
|
||||
render: (text: string) => {
|
||||
return <>{text || '-'}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'auth_name',
|
||||
title: '用户',
|
||||
width: 160,
|
||||
render: (text: string, record) => {
|
||||
return (
|
||||
<Box sx={{ fontSize: 12 }}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
{/* @ts-expect-error 类型不匹配 */}
|
||||
<img src={record?.avatar || Logo} width={16} />
|
||||
<Box sx={{ fontSize: 14 }}>{text || '匿名用户'}</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'created_at',
|
||||
title: '时间',
|
||||
width: 180,
|
||||
render: (text: string, record) => {
|
||||
return (
|
||||
<Stack>
|
||||
<Box>{dayjs(text).fromNow()}</Box>
|
||||
<Box sx={{ fontSize: 12, color: 'text.tertiary' }}>
|
||||
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '操作选项',
|
||||
width: 120,
|
||||
render: (text, record) => {
|
||||
const s =
|
||||
statusColorMap[record.status as keyof typeof statusColorMap];
|
||||
return record.status !==
|
||||
ConstsContributeStatus.ContributeStatusPending ? (
|
||||
<Chip
|
||||
label={s.label}
|
||||
color={s.color}
|
||||
variant='outlined'
|
||||
onClick={() => {
|
||||
setPreviewRow(record);
|
||||
setOpen(true);
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{ color: 'info.main', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setPreviewRow(record);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{s.label}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getData = () => {
|
||||
setLoading(true);
|
||||
getApiProV1ContributeList({
|
||||
page,
|
||||
per_page: pageSize,
|
||||
kb_id,
|
||||
node_name: nodeNameParam,
|
||||
auth_name: authNameParam,
|
||||
})
|
||||
.then(res => {
|
||||
setData(res.list || []);
|
||||
setTotal(res.total || 0);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (kb_id) getData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,11 +20,7 @@ import { putApiV1KnowledgeBaseDetail } from '@/request/KnowledgeBase';
|
|||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import { GithubComChaitinPandaWikiProApiAuthV1AuthItem } from '@/request/pro/types';
|
||||
import UserGroup from './UserGroup';
|
||||
import {
|
||||
getApiProV1AuthGet,
|
||||
postApiProV1AuthSet,
|
||||
deleteApiProV1AuthDelete,
|
||||
} from '@/request/pro/Auth';
|
||||
import { getApiProV1AuthGet, postApiProV1AuthSet } from '@/request/pro/Auth';
|
||||
|
||||
import { getApiV1AuthGet, postApiV1AuthSet } from '@/request/Auth';
|
||||
|
||||
|
|
@ -242,24 +238,6 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
getAuth();
|
||||
}, [kb_id, isPro, source_type, enabled]);
|
||||
|
||||
const onDeleteUser = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除用户',
|
||||
content: '确定要删除该用户吗?',
|
||||
okButtonProps: {
|
||||
color: 'error',
|
||||
},
|
||||
onOk: () => {
|
||||
deleteApiProV1AuthDelete({
|
||||
id,
|
||||
}).then(() => {
|
||||
message.success('删除成功');
|
||||
setMemberList(memberList.filter(item => item.id !== id));
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
|
||||
{
|
||||
title: '用户名',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { putApiV1App } from '@/request/App';
|
||||
import { useAppSelector } from '@/store';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import {
|
||||
|
|
@ -20,7 +19,7 @@ import { useEffect, useState } from 'react';
|
|||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { FormItem, SettingCard, SettingCardItem } from './Common';
|
||||
import { getApiV1AppDetail } from '@/request/App';
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
|
||||
interface CardCommentProps {
|
||||
kb: DomainKnowledgeBaseDetail;
|
||||
|
|
@ -344,6 +343,94 @@ const DocumentCorrection = ({
|
|||
);
|
||||
};
|
||||
|
||||
const DocumentContribution = ({
|
||||
data,
|
||||
refresh,
|
||||
}: {
|
||||
data: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enable: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
{ id: data.id! },
|
||||
{
|
||||
kb_id,
|
||||
settings: {
|
||||
...data.settings,
|
||||
contribute_settings: {
|
||||
is_enable: formData.is_enable,
|
||||
},
|
||||
},
|
||||
},
|
||||
).then(() => {
|
||||
message.success('保存成功');
|
||||
setIsEdit(false);
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
'is_enable',
|
||||
// @ts-expect-error 忽略类型错误
|
||||
data?.settings?.contribute_settings?.is_enable,
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title={
|
||||
<>
|
||||
文档贡献
|
||||
{!isPro && (
|
||||
<Tooltip title='联创版和企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
isEdit={isEdit}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_enable'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(e.target.value === 'true');
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
||||
const CardFeedback = ({ kb }: CardCommentProps) => {
|
||||
const [info, setInfo] = useState<DomainAppDetailResp | null>(null);
|
||||
|
||||
|
|
@ -363,6 +450,7 @@ const CardFeedback = ({ kb }: CardCommentProps) => {
|
|||
<AIQuestion data={info} refresh={getInfo} />
|
||||
<DocumentComments data={info} refresh={getInfo} />
|
||||
<DocumentCorrection data={info} refresh={getInfo} />
|
||||
<DocumentContribution data={info} refresh={getInfo} />
|
||||
</SettingCard>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import httpRequest, { ContentType, RequestParams } from './httpClient';
|
||||
import {
|
||||
DeleteApiV1AppParams,
|
||||
DomainAppDetailResp,
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
DomainUpdateAppReq,
|
||||
GetApiV1AppDetailParams,
|
||||
PutApiV1AppParams,
|
||||
} from "./types";
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* @description Update app
|
||||
|
|
@ -39,12 +39,12 @@ export const putApiV1App = (
|
|||
) =>
|
||||
httpRequest<DomainResponse>({
|
||||
path: `/api/v1/app`,
|
||||
method: "PUT",
|
||||
method: 'PUT',
|
||||
query: query,
|
||||
body: app,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ export const deleteApiV1App = (
|
|||
) =>
|
||||
httpRequest<DomainResponse>({
|
||||
path: `/api/v1/app`,
|
||||
method: "DELETE",
|
||||
method: 'DELETE',
|
||||
query: query,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
|
|
@ -96,10 +96,10 @@ export const getApiV1AppDetail = (
|
|||
}
|
||||
>({
|
||||
path: `/api/v1/app/detail`,
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
query: query,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import {
|
||||
DomainResponse,
|
||||
GetApiProV1ContributeDetailParams,
|
||||
GetApiProV1ContributeListParams,
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeAuditReq,
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeAuditResp,
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp,
|
||||
GithubComChaitinPandaWikiProApiContributeV1ContributeListResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description 审核文档贡献,支持通过或拒绝
|
||||
*
|
||||
* @tags Contribute
|
||||
* @name PostApiProV1ContributeAudit
|
||||
* @summary 审核贡献
|
||||
* @request POST:/api/pro/v1/contribute/audit
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiContributeV1ContributeAuditResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiProV1ContributeAudit = (
|
||||
param: GithubComChaitinPandaWikiProApiContributeV1ContributeAuditReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiContributeV1ContributeAuditResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/pro/v1/contribute/audit`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 根据ID获取文档贡献详情
|
||||
*
|
||||
* @tags Contribute
|
||||
* @name GetApiProV1ContributeDetail
|
||||
* @summary 获取贡献详情
|
||||
* @request GET:/api/pro/v1/contribute/detail
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getApiProV1ContributeDetail = (
|
||||
query: GetApiProV1ContributeDetailParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/pro/v1/contribute/detail`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取文档贡献列表,支持按知识库和状态筛选
|
||||
*
|
||||
* @tags Contribute
|
||||
* @name GetApiProV1ContributeList
|
||||
* @summary 获取贡献列表
|
||||
* @request GET:/api/pro/v1/contribute/list
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiContributeV1ContributeListResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getApiProV1ContributeList = (
|
||||
query: GetApiProV1ContributeListParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiContributeV1ContributeListResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/pro/v1/contribute/list`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -10,14 +10,14 @@
|
|||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import httpRequest, { ContentType, RequestParams } from './httpClient';
|
||||
import {
|
||||
DomainGetNodeReleaseDetailResp,
|
||||
DomainNodeReleaseListItem,
|
||||
DomainPWResponse,
|
||||
GetApiProV1NodeReleaseDetailParams,
|
||||
GetApiProV1NodeReleaseListParams,
|
||||
} from "./types";
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* @description Get Node Release Detail
|
||||
|
|
@ -42,10 +42,10 @@ export const getApiProV1NodeReleaseDetail = (
|
|||
}
|
||||
>({
|
||||
path: `/api/pro/v1/node/release/detail`,
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
||||
|
|
@ -72,9 +72,9 @@ export const getApiProV1NodeReleaseList = (
|
|||
}
|
||||
>({
|
||||
path: `/api/pro/v1/node/release/list`,
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import {
|
||||
DomainResponse,
|
||||
GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1SubmitContributeResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description 前台用户提交文档编辑或新增贡献
|
||||
*
|
||||
* @tags ShareContribute
|
||||
* @name PostShareProV1ContributeSubmit
|
||||
* @summary 提交文档贡献
|
||||
* @request POST:/share/pro/v1/contribute/submit
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1SubmitContributeResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareProV1ContributeSubmit = (
|
||||
param: GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1SubmitContributeResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/pro/v1/contribute/submit`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import {
|
||||
DomainResponse,
|
||||
GithubComChaitinPandaWikiProApiShareV1FileUploadResp,
|
||||
PostShareProV1FileUploadPayload,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description 前台用户上传文件
|
||||
*
|
||||
* @tags ShareFile
|
||||
* @name PostShareProV1FileUpload
|
||||
* @summary 文件上传
|
||||
* @request POST:/share/pro/v1/file/upload
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1FileUploadResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareProV1FileUpload = (
|
||||
data: PostShareProV1FileUploadPayload,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1FileUploadResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/pro/v1/file/upload`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.FormData,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -4,10 +4,13 @@ export * from './AuthGroup';
|
|||
export * from './AuthOrg';
|
||||
export * from './Block';
|
||||
export * from './Comment';
|
||||
export * from './Contribute';
|
||||
export * from './DocumentFeedback';
|
||||
export * from './License';
|
||||
export * from './Node';
|
||||
export * from './Prompt';
|
||||
export * from './ShareAuth';
|
||||
export * from './ShareContribute';
|
||||
export * from './ShareFile';
|
||||
export * from './ShareOpenapi';
|
||||
export * from './types';
|
||||
|
|
|
|||
|
|
@ -56,6 +56,17 @@ export enum ConstsLicenseEdition {
|
|||
LicenseEditionEnterprise = 2,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
ContributeTypeAdd = 'add',
|
||||
ContributeTypeEdit = 'edit',
|
||||
}
|
||||
|
||||
export enum ConstsContributeStatus {
|
||||
ContributeStatusPending = 'pending',
|
||||
ContributeStatusApproved = 'approved',
|
||||
ContributeStatusRejected = 'rejected',
|
||||
}
|
||||
|
||||
export interface DomainCommentModerateListReq {
|
||||
ids: string[];
|
||||
status: DomainCommentStatus;
|
||||
|
|
@ -309,6 +320,72 @@ export interface GithubComChaitinPandaWikiProApiAuthV1AuthSetReq {
|
|||
user_info_url?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeAuditReq {
|
||||
id: string;
|
||||
kb_id: string;
|
||||
parent_id?: string;
|
||||
position?: number;
|
||||
status: 'approved' | 'rejected';
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeAuditResp {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp {
|
||||
audit_time?: string;
|
||||
audit_user_id?: string;
|
||||
auth_id?: number;
|
||||
auth_name?: string;
|
||||
content?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
meta?: GithubComChaitinPandaWikiProApiContributeV1NodeMeta;
|
||||
node_id?: string;
|
||||
node_name?: string;
|
||||
/** edit类型时返回原始node信息 */
|
||||
original_node?: GithubComChaitinPandaWikiProApiContributeV1OriginalNodeInfo;
|
||||
reason?: string;
|
||||
status?: ConstsContributeStatus;
|
||||
type?: ConstsContributeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeItem {
|
||||
audit_time?: string;
|
||||
audit_user_id?: string;
|
||||
auth_id?: number;
|
||||
auth_name?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
meta?: GithubComChaitinPandaWikiProApiContributeV1NodeMeta;
|
||||
node_id?: string;
|
||||
node_name?: string;
|
||||
reason?: string;
|
||||
status?: ConstsContributeStatus;
|
||||
type?: ConstsContributeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeListResp {
|
||||
list?: GithubComChaitinPandaWikiProApiContributeV1ContributeItem[];
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1NodeMeta {
|
||||
doc_width?: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1OriginalNodeInfo {
|
||||
content?: string;
|
||||
id?: string;
|
||||
meta?: GithubComChaitinPandaWikiProApiContributeV1NodeMeta;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthCASReq {
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
|
|
@ -397,6 +474,10 @@ export type GithubComChaitinPandaWikiProApiShareV1FeishuCallbackResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1FileUploadResp {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1GitHubCallbackResp = Record<
|
||||
string,
|
||||
any
|
||||
|
|
@ -407,6 +488,22 @@ export type GithubComChaitinPandaWikiProApiShareV1OAuthCallbackResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq {
|
||||
content?: string;
|
||||
doc_width?: string;
|
||||
emoji?: string;
|
||||
kb_id?: string;
|
||||
name?: string;
|
||||
node_id?: string;
|
||||
reason?: string;
|
||||
type: 'add' | 'edit';
|
||||
}
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1SubmitContributeResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1WecomCallbackResp = Record<
|
||||
string,
|
||||
any
|
||||
|
|
@ -505,6 +602,22 @@ export interface GetApiProV1BlockParams {
|
|||
kb_id: string;
|
||||
}
|
||||
|
||||
export interface GetApiProV1ContributeDetailParams {
|
||||
id: string;
|
||||
kb_id: string;
|
||||
}
|
||||
|
||||
export interface GetApiProV1ContributeListParams {
|
||||
auth_name?: string;
|
||||
kb_id?: string;
|
||||
node_name?: string;
|
||||
/** @min 1 */
|
||||
page: number;
|
||||
/** @min 1 */
|
||||
per_page: number;
|
||||
status?: 'pending' | 'approved' | 'rejected';
|
||||
}
|
||||
|
||||
export interface GetApiProV1DocumentListParams {
|
||||
kb_id: string;
|
||||
/** @min 1 */
|
||||
|
|
@ -560,6 +673,11 @@ export interface PostShareProV1DocumentFeedbackPayload {
|
|||
image?: File;
|
||||
}
|
||||
|
||||
export interface PostShareProV1FileUploadPayload {
|
||||
/** File */
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface GetShareProV1OpenapiCasCallbackParams {
|
||||
state?: string;
|
||||
ticket?: string;
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ export interface DomainAppSettings {
|
|||
captcha_settings?: ConstsCaptchaSettings;
|
||||
/** catalog settings */
|
||||
catalog_settings?: DomainCatalogSettings;
|
||||
contribute_settings?: DomainContributeSettings;
|
||||
copy_setting?: '' | 'append' | 'disabled';
|
||||
/** seo */
|
||||
desc?: string;
|
||||
|
|
@ -316,6 +317,7 @@ export interface DomainAppSettingsResp {
|
|||
captcha_settings?: ConstsCaptchaSettings;
|
||||
/** catalog settings */
|
||||
catalog_settings?: DomainCatalogSettings;
|
||||
contribute_settings?: DomainContributeSettings;
|
||||
copy_setting?: ConstsCopySetting;
|
||||
/** seo */
|
||||
desc?: string;
|
||||
|
|
@ -453,6 +455,10 @@ export interface DomainCommentReq {
|
|||
user_name?: string;
|
||||
}
|
||||
|
||||
export interface DomainContributeSettings {
|
||||
is_enable?: boolean;
|
||||
}
|
||||
|
||||
export interface DomainConversationDetailResp {
|
||||
app_id?: string;
|
||||
created_at?: string;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ const router = [
|
|||
LazyLoadable(lazy(() => import('./pages/setting'))),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/contribution',
|
||||
element: createElement(
|
||||
LazyLoadable(lazy(() => import('./pages/contribution'))),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/release',
|
||||
element: createElement(
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ image: build
|
|||
.
|
||||
|
||||
save: image
|
||||
docker save -o ./panda-wiki-app_frontend.tar panda-wiki-app/frontend:main
|
||||
docker save -o /tmp/panda-wiki-app_frontend.tar panda-wiki-app/frontend:main
|
||||
|
||||
|
|
@ -12,8 +12,9 @@
|
|||
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,css,scss,md,mdx,json,yml,yaml,mjs,cjs}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@cap.js/widget": "^0.1.26",
|
||||
"@ctzhian/tiptap": "1.3.1",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@mui/material-nextjs": "^7.1.0",
|
||||
"@sentry/nextjs": "^10.8.0",
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
"highlight.js": "^11.11.1",
|
||||
"html-react-parser": "^5.2.5",
|
||||
"html-to-image": "^1.11.13",
|
||||
"import-in-the-middle": "^1.14.2",
|
||||
"katex": "^0.16.22",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it-highlightjs": "^4.2.0",
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"require-in-the-middle": "^7.5.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -50,4 +53,4 @@
|
|||
"eslint-config-prettier": "^9.1.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import DocEditor from '@/views/editor';
|
||||
|
||||
export default function EditorPage() {
|
||||
return <DocEditor />;
|
||||
}
|
||||
|
|
@ -18,8 +18,6 @@ const PCLayout = ({ children }: { children: React.ReactNode }) => {
|
|||
alignItems='flex-start'
|
||||
gap={'96px'}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
pt: '50px',
|
||||
pb: 10,
|
||||
px: 5,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ export interface KBDetail {
|
|||
social_media_accounts?: DomainSocialMediaAccount[];
|
||||
footer_show_intro?: boolean;
|
||||
};
|
||||
contribute_settings?: {
|
||||
is_enable: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface DomainSocialMediaAccount {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"search": "搜索",
|
||||
"search_no_results_1": "哦不!",
|
||||
"search_no_results_2": "没有找到相关表情",
|
||||
"pick": "选择一个表情…",
|
||||
"add_custom": "添加自定义表情",
|
||||
"categories": {
|
||||
"activity": "活动",
|
||||
"custom": "自定义",
|
||||
"flags": "旗帜",
|
||||
"foods": "食物与饮品",
|
||||
"frequent": "最近使用",
|
||||
"nature": "动物与自然",
|
||||
"objects": "物品",
|
||||
"people": "表情与角色",
|
||||
"places": "旅行与景点",
|
||||
"search": "搜索结果",
|
||||
"symbols": "符号"
|
||||
},
|
||||
"skins": {
|
||||
"choose": "选择默认肤色",
|
||||
"1": "默认",
|
||||
"2": "白色",
|
||||
"3": "偏白",
|
||||
"4": "中等",
|
||||
"5": "偏黑",
|
||||
"6": "黑色"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
'use client';
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
import { Box, IconButton, Popover, SxProps } from '@mui/material';
|
||||
import React, { useCallback } from 'react';
|
||||
import zh from './emoji-data/zh.json';
|
||||
import {
|
||||
IconWenjianjia,
|
||||
IconWenjianjiaKai,
|
||||
IconWenjian,
|
||||
} from '@panda-wiki/icons';
|
||||
|
||||
interface EmojiPickerProps {
|
||||
type: 1 | 2;
|
||||
readOnly?: boolean;
|
||||
value?: string;
|
||||
collapsed?: boolean;
|
||||
onChange?: (emoji: string) => void;
|
||||
sx?: SxProps;
|
||||
iconSx?: SxProps;
|
||||
}
|
||||
|
||||
const EmojiPicker: React.FC<EmojiPickerProps> = ({
|
||||
type,
|
||||
readOnly,
|
||||
value,
|
||||
onChange,
|
||||
collapsed,
|
||||
sx,
|
||||
iconSx,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
if (readOnly) return;
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(emoji: any) => {
|
||||
onChange?.(emoji.native);
|
||||
handleClose();
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'emoji-picker' : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
size='small'
|
||||
aria-describedby={id}
|
||||
disabled={readOnly}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
height: 28,
|
||||
color: 'text.primary',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{value ? (
|
||||
<Box component='span' sx={{ fontSize: 14, ...iconSx }}>
|
||||
{value}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{type === 1 ? (
|
||||
collapsed ? (
|
||||
<IconWenjianjia sx={{ fontSize: 16, ...iconSx }} />
|
||||
) : (
|
||||
<IconWenjianjiaKai sx={{ fontSize: 16, ...iconSx }} />
|
||||
)
|
||||
) : (
|
||||
<IconWenjian sx={{ fontSize: 16, ...iconSx }} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Picker
|
||||
data={data}
|
||||
set='native'
|
||||
theme='light'
|
||||
locale='zh'
|
||||
i18n={zh}
|
||||
onEmojiSelect={handleSelect}
|
||||
previewPosition='none'
|
||||
searchPosition='sticky'
|
||||
skinTonePosition='none'
|
||||
perLine={9}
|
||||
emojiSize={24}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiPicker;
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
import { Box, Popover, Stack, SxProps, Theme, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
interface Item {
|
||||
label: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
selected?: boolean;
|
||||
children?: Item[];
|
||||
show?: boolean;
|
||||
textSx?: SxProps<Theme>;
|
||||
key: number | string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface MenuSelectProps {
|
||||
id?: string;
|
||||
arrowIcon?: React.ReactNode;
|
||||
list: Item[];
|
||||
context?: React.ReactElement<{ onClick?: any; 'aria-describedby'?: any }>;
|
||||
anchorOrigin?: {
|
||||
vertical: 'top' | 'bottom' | 'center';
|
||||
horizontal: 'left' | 'right' | 'center';
|
||||
};
|
||||
transformOrigin?: {
|
||||
vertical: 'top' | 'bottom' | 'center';
|
||||
horizontal: 'left' | 'right' | 'center';
|
||||
};
|
||||
childrenProps?: {
|
||||
anchorOrigin?: {
|
||||
vertical: 'top' | 'bottom' | 'center';
|
||||
horizontal: 'left' | 'right' | 'center';
|
||||
};
|
||||
transformOrigin?: {
|
||||
vertical: 'top' | 'bottom' | 'center';
|
||||
horizontal: 'left' | 'right' | 'center';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const MenuSelect: React.FC<MenuSelectProps> = ({
|
||||
id = 'menu-select',
|
||||
arrowIcon,
|
||||
list,
|
||||
context,
|
||||
anchorOrigin = {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
},
|
||||
transformOrigin = {
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
},
|
||||
childrenProps = {
|
||||
anchorOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
},
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
||||
null,
|
||||
);
|
||||
const [hoveredItem, setHoveredItem] = React.useState<Item | null>(null);
|
||||
const [subMenuAnchor, setSubMenuAnchor] = React.useState<HTMLElement | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
setHoveredItem(null);
|
||||
setSubMenuAnchor(null);
|
||||
};
|
||||
|
||||
const handleItemHover = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
item: Item,
|
||||
) => {
|
||||
if (item.children?.length) {
|
||||
setHoveredItem(item);
|
||||
setSubMenuAnchor(event.currentTarget);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemLeave = () => {
|
||||
setHoveredItem(null);
|
||||
setSubMenuAnchor(null);
|
||||
};
|
||||
|
||||
const handleItemClick = (item: Item) => {
|
||||
if (item.onClick) {
|
||||
item.onClick();
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const curId = open ? id : undefined;
|
||||
return (
|
||||
<>
|
||||
{context &&
|
||||
React.cloneElement(context, {
|
||||
onClick: handleClick,
|
||||
'aria-describedby': curId,
|
||||
})}
|
||||
<Popover
|
||||
id={curId}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={anchorOrigin}
|
||||
transformOrigin={transformOrigin}
|
||||
>
|
||||
<Box className='menu-select-list' sx={{ p: 0.5 }}>
|
||||
{list.map(item =>
|
||||
item.show === false ? null : (
|
||||
<Box
|
||||
className='menu-select-item'
|
||||
key={item.key}
|
||||
onMouseEnter={e => handleItemHover(e, item)}
|
||||
onMouseLeave={handleItemLeave}
|
||||
onClick={() => handleItemClick(item)}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Stack alignItems='center' gap={1} direction='row'>
|
||||
{item.icon}
|
||||
<Typography
|
||||
variant='body1'
|
||||
sx={{ flexShrink: 0, ...item.textSx }}
|
||||
>
|
||||
{item.label}
|
||||
</Typography>
|
||||
{item.extra}
|
||||
{item.children?.length ? arrowIcon : null}
|
||||
</Stack>
|
||||
{hoveredItem === item && item.children && (
|
||||
<Popover
|
||||
open={Boolean(subMenuAnchor)}
|
||||
anchorEl={subMenuAnchor}
|
||||
onClose={handleItemLeave}
|
||||
sx={{ pointerEvents: 'none' }}
|
||||
{...childrenProps}
|
||||
>
|
||||
<Box
|
||||
className='menu-select-sub-list'
|
||||
sx={{
|
||||
pointerEvents: 'auto',
|
||||
p: 0.5,
|
||||
}}
|
||||
>
|
||||
{item.children.map(child =>
|
||||
child.show === false ? null : (
|
||||
<Box
|
||||
key={child.key}
|
||||
className='menu-select-sub-item'
|
||||
onClick={() => handleItemClick(child)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Stack alignItems='center' gap={1} direction='row'>
|
||||
{child.icon}
|
||||
<Typography
|
||||
sx={{ flexShrink: 0, ...child.textSx }}
|
||||
>
|
||||
{child.label}
|
||||
</Typography>
|
||||
{child.extra}
|
||||
</Stack>
|
||||
</Box>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
</Box>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuSelect;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from './httpClient';
|
||||
import {
|
||||
DomainPWResponse,
|
||||
GetShareV1OpenapiGithubCallbackParams,
|
||||
GithubComChaitinPandaWikiApiShareV1GitHubCallbackResp,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* @description GitHub回调
|
||||
*
|
||||
* @tags ShareOpenapi
|
||||
* @name GetShareV1OpenapiGithubCallback
|
||||
* @summary GitHub回调
|
||||
* @request GET:/share/v1/openapi/github/callback
|
||||
* @response `200` `(DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiApiShareV1GitHubCallbackResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const getShareV1OpenapiGithubCallback = (
|
||||
query: GetShareV1OpenapiGithubCallbackParams,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiApiShareV1GitHubCallbackResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/v1/openapi/github/callback`,
|
||||
method: 'GET',
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
export * from './ShareApp'
|
||||
export * from './ShareAuth'
|
||||
export * from './ShareCaptcha'
|
||||
export * from './ShareChat'
|
||||
export * from './ShareComment'
|
||||
export * from './ShareConversation'
|
||||
export * from './ShareNode'
|
||||
export * from './ShareStat'
|
||||
export * from './Wechat'
|
||||
export * from './types'
|
||||
|
||||
export * from './ShareApp';
|
||||
export * from './ShareAuth';
|
||||
export * from './ShareCaptcha';
|
||||
export * from './ShareChat';
|
||||
export * from './ShareComment';
|
||||
export * from './ShareConversation';
|
||||
export * from './ShareNode';
|
||||
export * from './ShareOpenapi';
|
||||
export * from './ShareStat';
|
||||
export * from './Wechat';
|
||||
export * from './types';
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@
|
|||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import httpRequest, { ContentType, RequestParams } from './httpClient';
|
||||
import {
|
||||
DomainGetNodeReleaseDetailResp,
|
||||
DomainNodeReleaseListItem,
|
||||
DomainPWResponse,
|
||||
GetApiProV1NodeReleaseDetailParams,
|
||||
GetApiProV1NodeReleaseListParams,
|
||||
} from "./types";
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* @description Get Node Release Detail
|
||||
|
|
@ -42,10 +42,10 @@ export const getApiProV1NodeReleaseDetail = (
|
|||
}
|
||||
>({
|
||||
path: `/api/pro/v1/node/release/detail`,
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
||||
|
|
@ -72,9 +72,9 @@ export const getApiProV1NodeReleaseList = (
|
|||
}
|
||||
>({
|
||||
path: `/api/pro/v1/node/release/list`,
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
query: query,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ export * from './ShareAuth';
|
|||
export * from './ShareContribute';
|
||||
export * from './ShareFile';
|
||||
export * from './ShareOpenapi';
|
||||
export * from './otherCustomer';
|
||||
export * from './types';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
|
||||
|
||||
import { RequestParams } from "./httpClient";
|
||||
import {
|
||||
GithubComChaitinPandaWikiProApiShareV1FileUploadResp,
|
||||
PostShareProV1FileUploadPayload,
|
||||
} from "./types";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 使用 XMLHttpRequest 实现文件上传进度
|
||||
*/
|
||||
export const postShareProV1FileUploadWithProgress = (
|
||||
data: PostShareProV1FileUploadPayload,
|
||||
params: RequestParams & {
|
||||
onprogress?: (progress: { progress: number }) => void;
|
||||
abortSignal?: AbortSignal;
|
||||
} = {},
|
||||
): Promise<GithubComChaitinPandaWikiProApiShareV1FileUploadResp> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { onprogress, abortSignal, ...requestParams } = params;
|
||||
|
||||
// 创建 FormData
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach(key => {
|
||||
const value = data[key as keyof PostShareProV1FileUploadPayload];
|
||||
if (value instanceof File) {
|
||||
formData.append(key, value);
|
||||
} else if (value !== null && value !== undefined) {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// 设置上传进度监听
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable && onprogress) {
|
||||
const progress = (event.loaded / event.total) * 100;
|
||||
onprogress({ progress });
|
||||
}
|
||||
});
|
||||
|
||||
// 设置响应处理
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.code === 0 || response.code === undefined) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(new Error(response.message || '上传失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error('响应解析失败'));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 设置错误处理
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('网络错误'));
|
||||
});
|
||||
|
||||
// 设置中止处理
|
||||
xhr.addEventListener('abort', () => {
|
||||
reject(new Error('请求被中止'));
|
||||
});
|
||||
|
||||
// 监听中止信号
|
||||
if (abortSignal) {
|
||||
abortSignal.addEventListener('abort', () => {
|
||||
xhr.abort();
|
||||
});
|
||||
}
|
||||
|
||||
// 构建请求 URL
|
||||
const baseUrl = process.env.TARGET || '';
|
||||
const url = `${baseUrl}/share/pro/v1/file/upload`;
|
||||
|
||||
// 发送请求
|
||||
xhr.open('POST', url);
|
||||
|
||||
// 设置请求头
|
||||
if (requestParams.headers) {
|
||||
Object.entries(requestParams.headers).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
xhr.setRequestHeader(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置凭据
|
||||
if (requestParams.credentials) {
|
||||
xhr.withCredentials = requestParams.credentials === 'include';
|
||||
}
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
};
|
||||
|
|
@ -56,6 +56,17 @@ export enum ConstsLicenseEdition {
|
|||
LicenseEditionEnterprise = 2,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
ContributeTypeAdd = 'add',
|
||||
ContributeTypeEdit = 'edit',
|
||||
}
|
||||
|
||||
export enum ConstsContributeStatus {
|
||||
ContributeStatusPending = 'pending',
|
||||
ContributeStatusApproved = 'approved',
|
||||
ContributeStatusRejected = 'rejected',
|
||||
}
|
||||
|
||||
export interface DomainCommentModerateListReq {
|
||||
ids: string[];
|
||||
status: DomainCommentStatus;
|
||||
|
|
@ -169,6 +180,7 @@ export interface GithubComChaitinPandaWikiProApiAuthV1AuthGetResp {
|
|||
/** LDAP特定配置 */
|
||||
ldap_server_url?: string;
|
||||
name_field?: string;
|
||||
proxy?: string;
|
||||
scopes?: string[];
|
||||
source_type?: ConstsSourceType;
|
||||
token_url?: string;
|
||||
|
|
@ -297,6 +309,7 @@ export interface GithubComChaitinPandaWikiProApiAuthV1AuthSetReq {
|
|||
/** LDAP特定配置 */
|
||||
ldap_server_url?: string;
|
||||
name_field?: string;
|
||||
proxy?: string;
|
||||
scopes?: string[];
|
||||
source_type?: ConstsSourceType;
|
||||
token_url?: string;
|
||||
|
|
@ -307,6 +320,72 @@ export interface GithubComChaitinPandaWikiProApiAuthV1AuthSetReq {
|
|||
user_info_url?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeAuditReq {
|
||||
id: string;
|
||||
kb_id: string;
|
||||
parent_id?: string;
|
||||
position?: number;
|
||||
status: 'approved' | 'rejected';
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeAuditResp {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeDetailResp {
|
||||
audit_time?: string;
|
||||
audit_user_id?: string;
|
||||
auth_id?: number;
|
||||
auth_name?: string;
|
||||
content?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
meta?: GithubComChaitinPandaWikiProApiContributeV1NodeMeta;
|
||||
node_id?: string;
|
||||
node_name?: string;
|
||||
/** edit类型时返回原始node信息 */
|
||||
original_node?: GithubComChaitinPandaWikiProApiContributeV1OriginalNodeInfo;
|
||||
reason?: string;
|
||||
status?: ConstsContributeStatus;
|
||||
type?: ConstsContributeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeItem {
|
||||
audit_time?: string;
|
||||
audit_user_id?: string;
|
||||
auth_id?: number;
|
||||
auth_name?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
meta?: GithubComChaitinPandaWikiProApiContributeV1NodeMeta;
|
||||
node_id?: string;
|
||||
node_name?: string;
|
||||
reason?: string;
|
||||
status?: ConstsContributeStatus;
|
||||
type?: ConstsContributeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1ContributeListResp {
|
||||
list?: GithubComChaitinPandaWikiProApiContributeV1ContributeItem[];
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1NodeMeta {
|
||||
doc_width?: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiContributeV1OriginalNodeInfo {
|
||||
content?: string;
|
||||
id?: string;
|
||||
meta?: GithubComChaitinPandaWikiProApiContributeV1NodeMeta;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthCASReq {
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
|
|
@ -395,6 +474,10 @@ export type GithubComChaitinPandaWikiProApiShareV1FeishuCallbackResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1FileUploadResp {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1GitHubCallbackResp = Record<
|
||||
string,
|
||||
any
|
||||
|
|
@ -405,6 +488,22 @@ export type GithubComChaitinPandaWikiProApiShareV1OAuthCallbackResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1SubmitContributeReq {
|
||||
content?: string;
|
||||
doc_width?: string;
|
||||
emoji?: string;
|
||||
kb_id?: string;
|
||||
name?: string;
|
||||
node_id?: string;
|
||||
reason?: string;
|
||||
type: 'add' | 'edit';
|
||||
}
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1SubmitContributeResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1WecomCallbackResp = Record<
|
||||
string,
|
||||
any
|
||||
|
|
@ -503,6 +602,22 @@ export interface GetApiProV1BlockParams {
|
|||
kb_id: string;
|
||||
}
|
||||
|
||||
export interface GetApiProV1ContributeDetailParams {
|
||||
id: string;
|
||||
kb_id: string;
|
||||
}
|
||||
|
||||
export interface GetApiProV1ContributeListParams {
|
||||
auth_name?: string;
|
||||
kb_id?: string;
|
||||
node_name?: string;
|
||||
/** @min 1 */
|
||||
page: number;
|
||||
/** @min 1 */
|
||||
per_page: number;
|
||||
status?: 'pending' | 'approved' | 'rejected';
|
||||
}
|
||||
|
||||
export interface GetApiProV1DocumentListParams {
|
||||
kb_id: string;
|
||||
/** @min 1 */
|
||||
|
|
@ -558,6 +673,11 @@ export interface PostShareProV1DocumentFeedbackPayload {
|
|||
image?: File;
|
||||
}
|
||||
|
||||
export interface PostShareProV1FileUploadPayload {
|
||||
/** File */
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface GetShareProV1OpenapiCasCallbackParams {
|
||||
state?: string;
|
||||
ticket?: string;
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ export interface DomainAppSettings {
|
|||
captcha_settings?: ConstsCaptchaSettings;
|
||||
/** catalog settings */
|
||||
catalog_settings?: DomainCatalogSettings;
|
||||
contribute_settings?: DomainContributeSettings;
|
||||
copy_setting?: '' | 'append' | 'disabled';
|
||||
/** seo */
|
||||
desc?: string;
|
||||
|
|
@ -316,6 +317,7 @@ export interface DomainAppSettingsResp {
|
|||
captcha_settings?: ConstsCaptchaSettings;
|
||||
/** catalog settings */
|
||||
catalog_settings?: DomainCatalogSettings;
|
||||
contribute_settings?: DomainContributeSettings;
|
||||
copy_setting?: ConstsCopySetting;
|
||||
/** seo */
|
||||
desc?: string;
|
||||
|
|
@ -453,6 +455,10 @@ export interface DomainCommentReq {
|
|||
user_name?: string;
|
||||
}
|
||||
|
||||
export interface DomainContributeSettings {
|
||||
is_enable?: boolean;
|
||||
}
|
||||
|
||||
export interface DomainConversationDetailResp {
|
||||
app_id?: string;
|
||||
created_at?: string;
|
||||
|
|
@ -1157,6 +1163,7 @@ export interface GithubComChaitinPandaWikiApiAuthV1AuthGetResp {
|
|||
auths?: V1AuthItem[];
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
proxy?: string;
|
||||
source_type?: ConstsSourceType;
|
||||
}
|
||||
|
||||
|
|
@ -1166,6 +1173,11 @@ export interface GithubComChaitinPandaWikiApiShareV1AuthGetResp {
|
|||
source_type?: ConstsSourceType;
|
||||
}
|
||||
|
||||
export type GithubComChaitinPandaWikiApiShareV1GitHubCallbackResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiDomainCheckModelReq {
|
||||
api_header?: string;
|
||||
api_key?: string;
|
||||
|
|
@ -1257,6 +1269,7 @@ export interface V1AuthSetReq {
|
|||
client_id?: string;
|
||||
client_secret?: string;
|
||||
kb_id?: string;
|
||||
proxy?: string;
|
||||
source_type: 'github';
|
||||
}
|
||||
|
||||
|
|
@ -1658,3 +1671,8 @@ export interface GetShareV1NodeDetailParams {
|
|||
/** format */
|
||||
format: string;
|
||||
}
|
||||
|
||||
export interface GetShareV1OpenapiGithubCallbackParams {
|
||||
code?: string;
|
||||
state?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ const createComponentStyleOverrides = (
|
|||
): CssVarsThemeOptions['components'] => ({
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
root: ({ theme }) => ({
|
||||
borderRadius: '10px !important',
|
||||
'.MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent',
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--mui-palette-text-primary) !important',
|
||||
borderWidth: '1px !important',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiSvgIcon: {
|
||||
|
|
|
|||
|
|
@ -265,3 +265,21 @@ export const getRedirectUrl = () => {
|
|||
: new URL('/', location.origin);
|
||||
return redirectUrl as URL;
|
||||
};
|
||||
|
||||
export const MAC_SYMBOLS = {
|
||||
ctrl: '⌘',
|
||||
alt: '⌥',
|
||||
shift: '⇧',
|
||||
};
|
||||
|
||||
export const isMac = () =>
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.platform.toLowerCase().includes('mac');
|
||||
|
||||
export const getShortcutKeyText = (shortcutKey: string[]) => {
|
||||
return shortcutKey
|
||||
?.map(it =>
|
||||
isMac() ? MAC_SYMBOLS[it as keyof typeof MAC_SYMBOLS] || it : it,
|
||||
)
|
||||
.join('+');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
import SSEClient from '@/utils/fetch';
|
||||
import { Box, Divider, Stack } from '@mui/material';
|
||||
import { Editor, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
|
||||
import { Modal } from '@ctzhian/ui';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface AIGenerateProps {
|
||||
open: boolean;
|
||||
selectText: string;
|
||||
onClose: () => void;
|
||||
editorRef: UseTiptapReturn;
|
||||
}
|
||||
|
||||
const AIGenerate = ({
|
||||
open,
|
||||
selectText,
|
||||
onClose,
|
||||
editorRef,
|
||||
}: AIGenerateProps) => {
|
||||
const sseClientRef = useRef<SSEClient<string> | null>(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const defaultEditor = useTiptap({
|
||||
editable: false,
|
||||
immediatelyRender: false,
|
||||
});
|
||||
|
||||
const readEditor = useTiptap({
|
||||
editable: false,
|
||||
immediatelyRender: false,
|
||||
});
|
||||
|
||||
const onGenerate = useCallback(() => {
|
||||
if (sseClientRef.current) {
|
||||
setLoading(true);
|
||||
sseClientRef.current.subscribe(
|
||||
JSON.stringify({
|
||||
text: selectText,
|
||||
action: 'rephrase',
|
||||
stream: true,
|
||||
}),
|
||||
data => {
|
||||
setContent(prev => {
|
||||
const newContent = prev + data;
|
||||
readEditor.editor?.commands.setContent(newContent);
|
||||
return newContent;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [selectText, sseClientRef.current, readEditor.editor]);
|
||||
|
||||
const onCancel = () => {
|
||||
sseClientRef.current?.unsubscribe();
|
||||
defaultEditor.editor.commands.setContent('');
|
||||
readEditor.editor.commands.setContent('');
|
||||
setContent('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
const { from, to } = editorRef.editor.state.selection;
|
||||
editorRef.editor.commands.insertContentAt({ from, to }, content);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
sseClientRef.current = new SSEClient<string>({
|
||||
url: '/api/v1/creation/text',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
onComplete: () => setLoading(false),
|
||||
onError: () => setLoading(false),
|
||||
});
|
||||
if (selectText) {
|
||||
defaultEditor.editor.commands.setContent(selectText);
|
||||
setTimeout(() => {
|
||||
onGenerate();
|
||||
}, 60);
|
||||
}
|
||||
}, [selectText, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultEditor.editor || !readEditor.editor) return;
|
||||
return () => {
|
||||
defaultEditor.editor.destroy();
|
||||
readEditor.editor.destroy();
|
||||
sseClientRef.current?.unsubscribe();
|
||||
};
|
||||
}, [defaultEditor, readEditor]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={'文本润色'}
|
||||
okText='替换'
|
||||
width={1000}
|
||||
onOk={onSubmit}
|
||||
okButtonProps={{
|
||||
loading,
|
||||
disabled: content.length === 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
sx={{
|
||||
'.tiptap.ProseMirror': {
|
||||
padding: '0px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
width: '50%',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
ml: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'text.auxiliary',
|
||||
}}
|
||||
>
|
||||
原文
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: '10px',
|
||||
p: 2,
|
||||
bgcolor: 'background.paper2',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Editor editor={defaultEditor.editor} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Divider orientation='vertical' flexItem sx={{ mx: 2 }} />
|
||||
<Stack sx={{ width: '50%', flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
ml: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'text.auxiliary',
|
||||
}}
|
||||
>
|
||||
润色后
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: '10px',
|
||||
p: 2,
|
||||
bgcolor: 'background.paper2',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Editor editor={readEditor.editor} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIGenerate;
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal } from '@ctzhian/ui';
|
||||
import { Box, TextField, Typography, styled } from '@mui/material';
|
||||
import { IconErrorCorrection } from '@/components/icons';
|
||||
|
||||
interface ConfirmModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: (reason: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const StyledInfoBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: theme.spacing(2),
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper2,
|
||||
borderRadius: theme.spacing(1.5),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledIconBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
flexShrink: 0,
|
||||
}));
|
||||
|
||||
const StyledContentBox = styled(Box)(({ theme }) => ({
|
||||
flex: 1,
|
||||
'& .title': {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
'& .description': {
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledLabel = styled(Typography)(({ theme }) => ({
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const ConfirmModal = ({ open, onCancel, onOk }: ConfirmModalProps) => {
|
||||
const [reason, setReason] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setReason('');
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
title='确认提交'
|
||||
okText='提交审核'
|
||||
onOk={() => onOk(reason)}
|
||||
>
|
||||
<StyledInfoBox>
|
||||
<StyledIconBox>
|
||||
<IconErrorCorrection sx={{ fontSize: 20 }} />
|
||||
</StyledIconBox>
|
||||
<StyledContentBox>
|
||||
<Typography className='title'>文档审核流程</Typography>
|
||||
<Typography className='description'>
|
||||
确认提交后,文档将进入审核状态。审核通过后将自动发布到知识库中,供其他用户查阅。
|
||||
</Typography>
|
||||
</StyledContentBox>
|
||||
</StyledInfoBox>
|
||||
|
||||
<StyledLabel>备注信息(可选)</StyledLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder='请输入备注信息,帮助审核人员更好地理解您的修改...'
|
||||
value={reason}
|
||||
onChange={e => setReason(e.target.value)}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmModal;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
'use client';
|
||||
import { V1NodeDetailResp } from '@/request/types';
|
||||
import { IconBaocun } from '@panda-wiki/icons';
|
||||
import { Box, Button, IconButton, Skeleton, Stack } from '@mui/material';
|
||||
import { Ellipsis, Icon, message } from '@ctzhian/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useWrapContext } from '..';
|
||||
|
||||
interface HeaderProps {
|
||||
edit: boolean;
|
||||
collaborativeUsers?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}>;
|
||||
isSyncing?: boolean;
|
||||
detail: V1NodeDetailResp;
|
||||
updateDetail: (detail: V1NodeDetailResp) => void;
|
||||
handleSave: () => void;
|
||||
handleExport: (type: string) => void;
|
||||
}
|
||||
|
||||
const Header = ({ edit, detail, handleSave }: HeaderProps) => {
|
||||
const firstLoad = useRef(true);
|
||||
|
||||
const { catalogOpen, nodeDetail, setCatalogOpen } = useWrapContext();
|
||||
|
||||
const [showSaveTip, setShowSaveTip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeDetail?.updated_at && !firstLoad.current) {
|
||||
setShowSaveTip(true);
|
||||
setTimeout(() => {
|
||||
setShowSaveTip(false);
|
||||
}, 1500);
|
||||
}
|
||||
firstLoad.current = false;
|
||||
}, [nodeDetail?.updated_at]);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ height: '40px' }}
|
||||
>
|
||||
{!catalogOpen && (
|
||||
<Stack
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
color: 'text.auxiliary',
|
||||
':hover': {
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
type='icon-muluzhankai'
|
||||
sx={{
|
||||
fontSize: 24,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack sx={{ width: 0, flex: 1 }}>
|
||||
{detail?.name ? (
|
||||
<Ellipsis sx={{ fontSize: 14, fontWeight: 'bold' }}>
|
||||
<Box
|
||||
component='span'
|
||||
sx={{ cursor: 'pointer' }}
|
||||
// onClick={() => setRenameOpen(true)}
|
||||
>
|
||||
{detail.name}
|
||||
</Box>
|
||||
</Ellipsis>
|
||||
) : // <Skeleton variant='text' width={300} height={24} />
|
||||
null}
|
||||
{nodeDetail?.updated_at && (
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ fontSize: 12, color: 'text.auxiliary' }}
|
||||
>
|
||||
<IconBaocun sx={{ fontSize: 12 }} />
|
||||
{nodeDetail?.updated_at ? (
|
||||
dayjs(nodeDetail.updated_at).format('YYYY-MM-DD HH:mm:ss')
|
||||
) : (
|
||||
<Skeleton variant='text' width={100} height={24} />
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack direction={'row'} gap={4}>
|
||||
<Button
|
||||
size='small'
|
||||
variant='contained'
|
||||
disabled={!detail.name}
|
||||
startIcon={<IconBaocun />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
'use client';
|
||||
import { Box, Skeleton, Stack } from '@mui/material';
|
||||
import { useTiptap } from '@ctzhian/tiptap';
|
||||
import { Icon } from '@ctzhian/ui';
|
||||
import { useState } from 'react';
|
||||
import Header from './Header';
|
||||
import Toolbar from './Toolbar';
|
||||
|
||||
const LoadingEditorWrap = () => {
|
||||
const [isSyncing] = useState(false);
|
||||
const [collaborativeUsers] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
const editorRef = useTiptap({
|
||||
editable: false,
|
||||
content: '',
|
||||
exclude: ['invisibleCharacters', 'youtube', 'mention'],
|
||||
immediatelyRender: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 2,
|
||||
transition: 'left 0.3s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
edit={false}
|
||||
isSyncing={isSyncing}
|
||||
collaborativeUsers={collaborativeUsers}
|
||||
detail={{}}
|
||||
updateDetail={() => {}}
|
||||
handleSave={() => {}}
|
||||
handleExport={() => {}}
|
||||
/>
|
||||
{editorRef.editor && <Toolbar editorRef={editorRef} />}
|
||||
</Box>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
p: '72px 72px 150px',
|
||||
mt: '102px',
|
||||
mx: 'auto',
|
||||
maxWidth: 892,
|
||||
minWidth: '386px',
|
||||
}}
|
||||
>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1} sx={{ mb: 2 }}>
|
||||
<Skeleton variant='text' width={36} height={36} />
|
||||
<Skeleton variant='text' width={300} height={36} />
|
||||
</Stack>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={2} sx={{ mb: 4 }}>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<Icon type='icon-a-shijian2' sx={{ color: 'text.auxiliary' }} />
|
||||
<Skeleton variant='text' width={130} height={24} />
|
||||
</Stack>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<Icon type='icon-ziti' sx={{ color: 'text.auxiliary' }} />
|
||||
<Skeleton variant='text' width={80} height={24} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={1}
|
||||
sx={{
|
||||
minHeight: 'calc(100vh - 432px)',
|
||||
}}
|
||||
>
|
||||
<Skeleton variant='text' height={24} />
|
||||
<Skeleton variant='text' width={300} height={24} />
|
||||
<Skeleton variant='text' height={24} />
|
||||
<Skeleton variant='text' height={24} />
|
||||
<Skeleton variant='text' width={600} height={24} />
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingEditorWrap;
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import { V1NodeDetailResp } from '@/request';
|
||||
import { Button, CircularProgress, Stack, TextField } from '@mui/material';
|
||||
import { Icon, message, Modal } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useWrapContext } from '..';
|
||||
|
||||
interface SummaryProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
updateDetail: (detail: V1NodeDetailResp) => void;
|
||||
}
|
||||
|
||||
const Summary = ({ open, onClose, updateDetail }: SummaryProps) => {
|
||||
const { nodeDetail } = useWrapContext();
|
||||
const [summary, setSummary] = useState(nodeDetail?.meta?.summary || '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [edit, setEdit] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setEdit(false);
|
||||
setSummary('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const createSummary = () => {
|
||||
if (!nodeDetail) return;
|
||||
setLoading(true);
|
||||
// postApiV1NodeSummary({ kb_id, ids: [nodeDetail.id!] })
|
||||
// .then(res => {
|
||||
// // @ts-expect-error 类型错误
|
||||
// setSummary(res.summary);
|
||||
// setEdit(true);
|
||||
// })
|
||||
// .finally(() => {
|
||||
// setLoading(false);
|
||||
// });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSummary(nodeDetail?.meta?.summary || '');
|
||||
}
|
||||
}, [open, nodeDetail]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
title='智能摘要'
|
||||
okText='保存'
|
||||
okButtonProps={{
|
||||
disabled: loading || !edit,
|
||||
}}
|
||||
onOk={() => {
|
||||
if (!nodeDetail) return;
|
||||
updateDetail({
|
||||
meta: {
|
||||
...nodeDetail?.meta,
|
||||
summary,
|
||||
},
|
||||
});
|
||||
// putApiV1NodeDetail({ id: nodeDetail.id!, kb_id, summary }).then(() => {
|
||||
// message.success('保存成功');
|
||||
// });
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<TextField
|
||||
autoFocus
|
||||
multiline
|
||||
disabled={loading}
|
||||
rows={10}
|
||||
fullWidth
|
||||
value={summary}
|
||||
onChange={e => {
|
||||
setSummary(e.target.value);
|
||||
setEdit(true);
|
||||
}}
|
||||
placeholder='请输入摘要'
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
onClick={createSummary}
|
||||
disabled={loading}
|
||||
startIcon={
|
||||
loading ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<Icon type='icon-DJzhinengzhaiyao' sx={{ fontSize: 16 }} />
|
||||
)
|
||||
}
|
||||
>
|
||||
点击此处,AI 自动生成摘要
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Summary;
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
import { Box, Drawer, IconButton, Stack } from '@mui/material';
|
||||
import {
|
||||
H1Icon,
|
||||
H2Icon,
|
||||
H3Icon,
|
||||
H4Icon,
|
||||
H5Icon,
|
||||
H6Icon,
|
||||
TocList,
|
||||
} from '@ctzhian/tiptap';
|
||||
import { Ellipsis, Icon } from '@ctzhian/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface TocProps {
|
||||
headings: TocList;
|
||||
fixed: boolean;
|
||||
setFixed: (fixed: boolean) => void;
|
||||
}
|
||||
|
||||
const HeadingIcon = [
|
||||
<H1Icon sx={{ fontSize: 12 }} key='h1' />,
|
||||
<H2Icon sx={{ fontSize: 12 }} key='h2' />,
|
||||
<H3Icon sx={{ fontSize: 12 }} key='h3' />,
|
||||
<H4Icon sx={{ fontSize: 12 }} key='h4' />,
|
||||
<H5Icon sx={{ fontSize: 12 }} key='h5' />,
|
||||
<H6Icon sx={{ fontSize: 12 }} key='h6' />,
|
||||
];
|
||||
|
||||
const HeadingSx = [
|
||||
{ fontSize: 14, fontWeight: 700, color: 'text.secondary' },
|
||||
{ fontSize: 14, fontWeight: 400, color: 'text.auxiliary' },
|
||||
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
||||
];
|
||||
|
||||
const Toc = ({ headings, fixed, setFixed }: TocProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const levels = Array.from(
|
||||
new Set(headings.map(it => it.level).sort((a, b) => a - b)),
|
||||
).slice(0, 3);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!open && (
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 110,
|
||||
right: 0,
|
||||
width: 56,
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={1.5}
|
||||
alignItems={'flex-end'}
|
||||
sx={{ mt: 10 }}
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
>
|
||||
{headings
|
||||
.filter(it => levels.includes(it.level))
|
||||
.map(it => {
|
||||
return (
|
||||
<Box
|
||||
key={it.id}
|
||||
sx={{
|
||||
width: 25 - (it.level - 1) * 5,
|
||||
height: 4,
|
||||
borderRadius: '2px',
|
||||
bgcolor: it.isActive
|
||||
? 'action.active'
|
||||
: it.isScrolledOver
|
||||
? 'action.selected'
|
||||
: 'action.hover',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
<Drawer
|
||||
variant={'persistent'}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onMouseLeave={() => {
|
||||
if (!fixed) setOpen(false);
|
||||
}}
|
||||
anchor='right'
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
zIndex: 2,
|
||||
top: 110,
|
||||
width: 292,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
p: 1,
|
||||
boxShadow: 'none !important',
|
||||
mt: '102px',
|
||||
bgcolor: 'background.default',
|
||||
width: 292,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'text.auxiliary',
|
||||
mb: 1,
|
||||
p: 1,
|
||||
pb: 0,
|
||||
}}
|
||||
>
|
||||
<Box>内容大纲</Box>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={() => {
|
||||
if (fixed) {
|
||||
setOpen(false);
|
||||
}
|
||||
setFixed(!fixed);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
type={!fixed ? 'icon-dingzi' : 'icon-icon_tool_close'}
|
||||
sx={{ fontSize: 18 }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={1}
|
||||
sx={{
|
||||
height: 'calc(100% - 146px)',
|
||||
overflowY: 'auto',
|
||||
p: 1,
|
||||
pt: 0,
|
||||
}}
|
||||
>
|
||||
{headings
|
||||
.filter(it => levels.includes(it.level))
|
||||
.map(it => {
|
||||
const idx = levels.indexOf(it.level);
|
||||
return (
|
||||
<Stack
|
||||
key={it.id}
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
ml: idx * 2,
|
||||
...HeadingSx[idx],
|
||||
color: it.isActive
|
||||
? 'primary.main'
|
||||
: (HeadingSx[idx]?.color ?? 'inherit'),
|
||||
}}
|
||||
onClick={() => {
|
||||
const element = document.getElementById(it.id);
|
||||
if (element) {
|
||||
const offset = 100;
|
||||
const elementPosition =
|
||||
element.getBoundingClientRect().top;
|
||||
const offsetPosition =
|
||||
elementPosition + window.pageYOffset - offset;
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
color: 'text.disabled',
|
||||
flexShrink: 0,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{HeadingIcon[it.level]}
|
||||
</Box>
|
||||
<Ellipsis arrow sx={{ flex: 1, width: 0 }}>
|
||||
{it.textContent}
|
||||
</Ellipsis>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toc;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
'use client';
|
||||
import { Box } from '@mui/material';
|
||||
import {
|
||||
AiGenerate2Icon,
|
||||
EditorToolbar,
|
||||
UseTiptapReturn,
|
||||
} from '@ctzhian/tiptap';
|
||||
|
||||
interface ToolbarProps {
|
||||
editorRef: UseTiptapReturn;
|
||||
handleAiGenerate?: () => void;
|
||||
}
|
||||
|
||||
const Toolbar = ({ editorRef, handleAiGenerate }: ToolbarProps) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 'auto',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: '10px',
|
||||
bgcolor: 'background.default',
|
||||
px: 0.5,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
{editorRef.editor && (
|
||||
<EditorToolbar
|
||||
editor={editorRef.editor}
|
||||
menuInToolbarMore={[
|
||||
{
|
||||
id: 'ai',
|
||||
label: '文本润色',
|
||||
icon: <AiGenerate2Icon sx={{ fontSize: '1rem' }} />,
|
||||
onClick: handleAiGenerate,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
'use client';
|
||||
import { postShareProV1FileUploadWithProgress } from '@/request/pro/otherCustomer';
|
||||
import Emoji from '@/components/emoji';
|
||||
import { V1NodeDetailResp } from '@/request/types';
|
||||
import { Box, Stack, TextField } from '@mui/material';
|
||||
import { Editor, TocList, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { IconZiti, IconAShijian2 } from '@panda-wiki/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useWrapContext } from '..';
|
||||
import AIGenerate from './AIGenerate';
|
||||
import Header from './Header';
|
||||
import Toc from './Toc';
|
||||
import Toolbar from './Toolbar';
|
||||
import ConfirmModal from './ConfirmModal';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
interface WrapProps {
|
||||
detail: V1NodeDetailResp;
|
||||
}
|
||||
|
||||
const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||
const { catalogOpen, nodeDetail, setNodeDetail, onSave } = useWrapContext();
|
||||
const { id } = useParams();
|
||||
|
||||
const [characterCount, setCharacterCount] = useState(0);
|
||||
const [headings, setHeadings] = useState<TocList>([]);
|
||||
const [fixedToc, setFixedToc] = useState(false);
|
||||
const [selectionText, setSelectionText] = useState('');
|
||||
const [aiGenerateOpen, setAiGenerateOpen] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
|
||||
const updateDetail = (value: V1NodeDetailResp) => {
|
||||
setNodeDetail({
|
||||
...nodeDetail,
|
||||
updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
status: 1,
|
||||
...value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = async (type: string) => {
|
||||
if (type === 'html') {
|
||||
const html = editorRef.getHTML();
|
||||
if (!html) return;
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${nodeDetail?.name}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
message.success('导出成功');
|
||||
}
|
||||
if (type === 'md') {
|
||||
const markdown = editorRef.getMarkdownByJSON();
|
||||
if (!markdown) return;
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${nodeDetail?.name}.md`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
message.success('导出成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (
|
||||
file: File,
|
||||
onProgress?: (progress: { progress: number }) => void,
|
||||
abortSignal?: AbortSignal,
|
||||
) => {
|
||||
const { key } = await postShareProV1FileUploadWithProgress(
|
||||
{ file },
|
||||
{
|
||||
onprogress: ({ progress }) => {
|
||||
onProgress?.({ progress: progress / 100 });
|
||||
},
|
||||
abortSignal,
|
||||
},
|
||||
);
|
||||
return Promise.resolve('/static-file/' + key);
|
||||
};
|
||||
|
||||
const handleTocUpdate = (toc: TocList) => {
|
||||
setHeadings(toc);
|
||||
};
|
||||
|
||||
const handleError = (error: Error) => {
|
||||
if (error.message) {
|
||||
message.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = ({ editor }: { editor: UseTiptapReturn['editor'] }) => {
|
||||
setIsEditing(true);
|
||||
setCharacterCount((editor.storage as any).characterCount.characters());
|
||||
};
|
||||
|
||||
const editorRef = useTiptap({
|
||||
editable: true,
|
||||
immediatelyRender: false,
|
||||
content: defaultDetail?.content || '',
|
||||
exclude: ['invisibleCharacters', 'youtube', 'mention'],
|
||||
onCreate: ({ editor: tiptapEditor }) => {
|
||||
setCharacterCount(
|
||||
(tiptapEditor.storage as any).characterCount.characters(),
|
||||
);
|
||||
},
|
||||
onError: handleError,
|
||||
onUpload: handleUpload,
|
||||
onUpdate: handleUpdate,
|
||||
onTocUpdate: handleTocUpdate,
|
||||
});
|
||||
|
||||
const handleAiGenerate = useCallback(() => {
|
||||
if (editorRef.editor) {
|
||||
const { from, to } = editorRef.editor.state.selection;
|
||||
const text = editorRef.editor.state.doc.textBetween(from, to, '\n');
|
||||
if (!text) {
|
||||
message.error('请先选择文本');
|
||||
return;
|
||||
}
|
||||
setSelectionText(text);
|
||||
setAiGenerateOpen(true);
|
||||
}
|
||||
}, [editorRef.editor]);
|
||||
|
||||
const handleGlobalSave = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
if (editorRef && editorRef.editor) {
|
||||
const html = editorRef.getHTML();
|
||||
updateDetail({
|
||||
content: html,
|
||||
});
|
||||
setConfirmModalOpen(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[editorRef, onSave],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleGlobalSave);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleGlobalSave);
|
||||
};
|
||||
}, [handleGlobalSave]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (editorRef) editorRef.editor?.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
bgcolor: 'background.default',
|
||||
transition: 'left 0.3s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
edit={isEditing}
|
||||
detail={nodeDetail!}
|
||||
updateDetail={updateDetail}
|
||||
handleSave={async () => {
|
||||
setConfirmModalOpen(true);
|
||||
}}
|
||||
handleExport={handleExport}
|
||||
/>
|
||||
<Toolbar editorRef={editorRef} handleAiGenerate={handleAiGenerate} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
...(fixedToc && {
|
||||
display: 'flex',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: `calc(100vw - 160px - ${fixedToc ? 292 : 0}px)`,
|
||||
p: '72px 80px 150px',
|
||||
mt: '102px',
|
||||
mx: 'auto',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{ mb: 2, position: 'relative' }}
|
||||
>
|
||||
<Emoji
|
||||
type={2}
|
||||
sx={{ flexShrink: 0, width: 36, height: 36 }}
|
||||
iconSx={{ fontSize: 28 }}
|
||||
value={nodeDetail?.meta?.emoji}
|
||||
readOnly={!!id}
|
||||
onChange={value => {
|
||||
setNodeDetail({
|
||||
...nodeDetail,
|
||||
meta: {
|
||||
...nodeDetail?.meta,
|
||||
emoji: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
sx={{ flex: 1 }}
|
||||
value={nodeDetail?.name}
|
||||
placeholder='请输入文档名称'
|
||||
slotProps={{
|
||||
input: {
|
||||
readOnly: !!id,
|
||||
sx: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
bgcolor: 'background.default',
|
||||
'& input': {
|
||||
p: 0,
|
||||
lineHeight: '36px',
|
||||
height: '36px',
|
||||
},
|
||||
'& fieldset': {
|
||||
border: 'none !important',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChange={e => {
|
||||
setNodeDetail({
|
||||
...nodeDetail,
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={2} sx={{ mb: 4 }}>
|
||||
{defaultDetail?.created_at && (
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'text.auxiliary',
|
||||
cursor: 'text',
|
||||
':hover': {
|
||||
color: 'text.auxiliary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconAShijian2 />
|
||||
{dayjs(defaultDetail?.created_at).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}{' '}
|
||||
创建
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ fontSize: 12, color: 'text.auxiliary' }}
|
||||
>
|
||||
<IconZiti />
|
||||
{characterCount} 字
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
wordBreak: 'break-all',
|
||||
'.tiptap.ProseMirror': {
|
||||
overflowX: 'hidden',
|
||||
minHeight: 'calc(100vh - 102px - 48px)',
|
||||
},
|
||||
'.tableWrapper': {
|
||||
maxWidth: `calc(100vw - 160px - ${catalogOpen ? 292 : 0}px - ${fixedToc ? 292 : 0}px)`,
|
||||
overflowX: 'auto',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{editorRef.editor && <Editor editor={editorRef.editor} />}
|
||||
</Box>
|
||||
</Box>
|
||||
<Toc headings={headings} fixed={fixedToc} setFixed={setFixedToc} />
|
||||
</Box>
|
||||
|
||||
<AIGenerate
|
||||
open={aiGenerateOpen}
|
||||
selectText={selectionText}
|
||||
onClose={() => setAiGenerateOpen(false)}
|
||||
editorRef={editorRef}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
open={confirmModalOpen}
|
||||
onCancel={() => setConfirmModalOpen(false)}
|
||||
onOk={async (reason: string) => {
|
||||
const value = editorRef.getHTML();
|
||||
updateDetail({
|
||||
content: value,
|
||||
});
|
||||
await onSave(value, reason);
|
||||
setConfirmModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wrap;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export const DocWidth = {
|
||||
full: {
|
||||
label: '全宽',
|
||||
value: 0,
|
||||
},
|
||||
wide: {
|
||||
label: '超宽',
|
||||
value: 1127,
|
||||
},
|
||||
normal: {
|
||||
label: '常规',
|
||||
value: 892,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
'use client';
|
||||
import { getShareV1NodeDetail } from '@/request/ShareNode';
|
||||
import { V1NodeDetailResp } from '@/request/types';
|
||||
import { Box } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useWrapContext } from '..';
|
||||
import LoadingEditorWrap from './Loading';
|
||||
import EditorWrap from './Wrap';
|
||||
|
||||
const Edit = () => {
|
||||
const { id = '' } = useParams();
|
||||
const { setNodeDetail, nodeDetail } = useWrapContext();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detail, setDetail] = useState<V1NodeDetailResp | null>(nodeDetail);
|
||||
|
||||
const getDetail = () => {
|
||||
setLoading(true);
|
||||
// @ts-expect-error 类型错误
|
||||
getShareV1NodeDetail({
|
||||
id: id[0] as string,
|
||||
})
|
||||
.then(res => {
|
||||
setDetail(res as V1NodeDetailResp);
|
||||
setNodeDetail(res as V1NodeDetailResp);
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}, 0);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getDetail();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
flexGrow: 1,
|
||||
/* Give a remote user a caret */
|
||||
'& .collaboration-carets__caret': {
|
||||
borderLeft: '1px solid #fff',
|
||||
borderRight: '1px solid #fff',
|
||||
marginLeft: '-1px',
|
||||
marginRight: '-1px',
|
||||
pointerEvents: 'none',
|
||||
position: 'relative',
|
||||
wordBreak: 'normal',
|
||||
},
|
||||
/* Render the username above the caret */
|
||||
'& .collaboration-carets__label': {
|
||||
borderRadius: '0 3px 3px 3px',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '600',
|
||||
left: '-1px',
|
||||
lineHeight: 'normal',
|
||||
padding: '0.1rem 0.3rem',
|
||||
position: 'absolute',
|
||||
top: '1.4em',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? <LoadingEditorWrap /> : <EditorWrap detail={detail!} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
'use client';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { postShareProV1ContributeSubmit } from '@/request/pro/ShareContribute';
|
||||
import { V1NodeDetailResp } from '@/request/types';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { Box, Stack, useMediaQuery } from '@mui/material';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Edit from './edit';
|
||||
|
||||
export interface WrapContext {
|
||||
catalogOpen: boolean;
|
||||
setCatalogOpen: (open: boolean) => void;
|
||||
nodeDetail: V1NodeDetailResp | null;
|
||||
setNodeDetail: (detail: V1NodeDetailResp) => void;
|
||||
onSave: (content: string, reason: string) => void;
|
||||
}
|
||||
|
||||
const WrapContext = createContext<WrapContext | undefined>(undefined);
|
||||
|
||||
export const useWrapContext = () => {
|
||||
const context = useContext(WrapContext);
|
||||
if (!context) {
|
||||
throw new Error('useWrapContext must be used within a WrapContextProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const DocEditor = () => {
|
||||
const { id } = useParams();
|
||||
const isWideScreen = useMediaQuery('(min-width:1400px)');
|
||||
const [nodeDetail, setNodeDetail] = useState<V1NodeDetailResp | null>(
|
||||
id
|
||||
? null
|
||||
: {
|
||||
name: '',
|
||||
content: '',
|
||||
},
|
||||
);
|
||||
const [catalogOpen, setCatalogOpen] = useState(true);
|
||||
|
||||
const onSave = (content: string, reason: string) => {
|
||||
return postShareProV1ContributeSubmit({
|
||||
node_id: id ? id[0] : undefined,
|
||||
name: nodeDetail!.name,
|
||||
content,
|
||||
type: id ? 'edit' : 'add',
|
||||
reason,
|
||||
emoji: nodeDetail?.meta?.emoji,
|
||||
}).then(() => {
|
||||
message.success('保存成功, 即将关闭页面');
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 优先尝试直接关闭当前窗口
|
||||
window.close();
|
||||
|
||||
// 若浏览器阻止关闭,则尽量离开当前页
|
||||
setTimeout(() => {
|
||||
// 仍未关闭时,尝试回退;若无历史则跳首页
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
} else {
|
||||
// 某些浏览器可通过替换为 _self 再关闭
|
||||
try {
|
||||
window.open('', '_self');
|
||||
window.close();
|
||||
} catch {}
|
||||
// 最终兜底:跳转到首页
|
||||
setTimeout(() => {
|
||||
if (!document.hidden) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}, 0);
|
||||
} catch {}
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCatalogOpen(isWideScreen);
|
||||
}, [isWideScreen]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction='row'
|
||||
sx={{ color: 'text.primary', bgcolor: 'background.default' }}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<WrapContext.Provider
|
||||
value={{
|
||||
catalogOpen,
|
||||
setCatalogOpen,
|
||||
nodeDetail,
|
||||
setNodeDetail,
|
||||
onSave,
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</WrapContext.Provider>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocEditor;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import Emoji from '@/components/emoji';
|
||||
import { postShareProV1ContributeSubmit } from '@/request/pro/ShareContribute';
|
||||
import { Box, TextField } from '@mui/material';
|
||||
import { message, Modal } from '@ctzhian/ui';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
interface AddNodeModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
const AddNodeModal = ({ open, onCancel }: AddNodeModalProps) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<{ name: string; emoji: string }>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
emoji: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const submit = (value: { name: string; emoji: string }) => {
|
||||
postShareProV1ContributeSubmit({
|
||||
name: value.name,
|
||||
content: '',
|
||||
reason: '',
|
||||
type: 'add',
|
||||
}).then(({ id }) => {
|
||||
message.success('创建成功');
|
||||
reset();
|
||||
handleClose();
|
||||
window.open(`/doc/editor/${id}`, '_blank');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='创建文档'
|
||||
open={open}
|
||||
width={600}
|
||||
okText='创建'
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit(submit)}
|
||||
>
|
||||
<Box sx={{ fontSize: 14, lineHeight: '36px' }}>文档名称</Box>
|
||||
<Controller
|
||||
control={control}
|
||||
name='name'
|
||||
rules={{ required: `请输入文档名称` }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
autoFocus
|
||||
size='small'
|
||||
placeholder={`请输入文档名称`}
|
||||
error={!!errors.name}
|
||||
helperText={errors.name?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Box sx={{ fontSize: 14, lineHeight: '36px', mt: 1 }}>文档图标</Box>
|
||||
<Controller
|
||||
control={control}
|
||||
name='emoji'
|
||||
render={({ field }) => <Emoji {...field} type={2} />}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNodeModal;
|
||||
|
|
@ -3,19 +3,24 @@
|
|||
import { NodeDetail } from '@/assets/type';
|
||||
import useCopy from '@/hooks/useCopy';
|
||||
import { useStore } from '@/provider';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ConstsCopySetting } from '@/request/types';
|
||||
import { TocList, useTiptap } from '@ctzhian/tiptap';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import { Fab, Zoom } from '@mui/material';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Fab, Zoom, Stack } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DocAnchor from './DocAnchor';
|
||||
import DocContent from './DocContent';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
const Doc = ({ node }: { node?: NodeDetail }) => {
|
||||
const { kbDetail, mobile } = useStore();
|
||||
const [headings, setHeadings] = useState<TocList>([]);
|
||||
const [characterCount, setCharacterCount] = useState(0);
|
||||
|
||||
const params = useParams() || {};
|
||||
const docId = params.id as string;
|
||||
const editorRef = useTiptap({
|
||||
content: node?.content || '',
|
||||
editable: false,
|
||||
|
|
@ -33,6 +38,7 @@ const Doc = ({ node }: { node?: NodeDetail }) => {
|
|||
}, [kbDetail]);
|
||||
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
|
||||
useCopy({
|
||||
mode:
|
||||
|
|
@ -47,16 +53,25 @@ const Doc = ({ node }: { node?: NodeDetail }) => {
|
|||
});
|
||||
|
||||
const handleScroll = () => {
|
||||
setShowScrollTop(window.scrollY > 300);
|
||||
setShowScrollTop(
|
||||
document.querySelector('#scroll-container')!.scrollTop > 300,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
document
|
||||
.querySelector('#scroll-container')!
|
||||
.addEventListener('scroll', handleScroll);
|
||||
return () =>
|
||||
document
|
||||
.querySelector('#scroll-container')!
|
||||
.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
document
|
||||
.querySelector('#scroll-container')!
|
||||
.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -78,22 +93,78 @@ const Doc = ({ node }: { node?: NodeDetail }) => {
|
|||
|
||||
{!mobile && <DocAnchor headings={headings} />}
|
||||
|
||||
<Zoom in={showScrollTop}>
|
||||
<Fab
|
||||
size='small'
|
||||
onClick={scrollToTop}
|
||||
{!mobile && kbDetail?.settings.contribute_settings?.is_enable && (
|
||||
<Stack
|
||||
gap={1}
|
||||
sx={{
|
||||
backgroundColor: 'background.paper3',
|
||||
color: 'text.primary',
|
||||
position: 'fixed',
|
||||
bottom: 66,
|
||||
bottom: 20,
|
||||
right: 16,
|
||||
zIndex: 1000,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
>
|
||||
<KeyboardArrowUpIcon sx={{ fontSize: 24 }} />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
<Zoom
|
||||
in={showActions}
|
||||
style={{ transitionDelay: showActions ? '100ms' : '0ms' }}
|
||||
>
|
||||
<Fab
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
window.open(`/editor`, '_blank');
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
<Zoom
|
||||
in={showActions}
|
||||
style={{ transitionDelay: showActions ? '40ms' : '0ms' }}
|
||||
>
|
||||
<Fab
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
window.open(`/editor/${docId}`, '_blank');
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
<Fab
|
||||
size='small'
|
||||
sx={{
|
||||
backgroundColor: 'background.paper2',
|
||||
color: 'text.secondary',
|
||||
'&:hover': { backgroundColor: 'background.paper2' },
|
||||
}}
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
>
|
||||
<MenuIcon
|
||||
sx={{
|
||||
transition: 'transform 200ms',
|
||||
transform: showActions ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
}}
|
||||
/>
|
||||
</Fab>
|
||||
<Zoom in={showScrollTop}>
|
||||
<Fab
|
||||
size='small'
|
||||
onClick={scrollToTop}
|
||||
sx={{
|
||||
backgroundColor: 'background.paper3',
|
||||
color: 'text.primary',
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.paper2',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<KeyboardArrowUpIcon sx={{ fontSize: 24 }} />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"packageManager": "pnpm@10.12.1",
|
||||
"dependencies": {
|
||||
"@ctzhian/ui": "^7.0.5",
|
||||
"@ctzhian/tiptap": "1.3.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
|
|
@ -42,4 +43,4 @@
|
|||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconFuzhi1 = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M833.33 767.96h-91.9c-21.73 0-39.34-17.6-39.34-39.34s17.62-39.34 39.34-39.34h91.9c8.82 0 15.98-7.18 15.98-15.98V193.8c0-8.8-7.17-15.98-15.98-15.98H353.84c-8.82 0-15.98 7.18-15.98 15.98v90.86c0 21.75-17.62 39.34-39.34 39.34s-39.34-17.6-39.34-39.34V193.8c0-52.21 42.47-94.67 94.67-94.67h479.49c52.19 0 94.67 42.45 94.67 94.67v479.49c-0.01 52.21-42.49 94.67-94.68 94.67z'></path>
|
||||
<path d='M675.96 925.33H196.47c-52.19 0-94.67-42.45-94.67-94.67V351.17c0-52.21 42.47-94.67 94.67-94.67h479.49c52.19 0 94.67 42.45 94.67 94.67v479.49c-0.01 52.22-42.48 94.67-94.67 94.67zM196.47 335.19c-8.82 0-15.98 7.18-15.98 15.98v479.49c0 8.8 7.17 15.98 15.98 15.98h479.49c8.82 0 15.98-7.18 15.98-15.98V351.17c0-8.8-7.17-15.98-15.98-15.98H196.47z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconFuzhi1.displayName = 'icon-fuzhi1';
|
||||
|
||||
export default IconFuzhi1;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconGongxian = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M965.22509829 246.61344053c19.4864257 0 34.73090838 15.77472557 34.7309084 35.26115127 0 146.08191235-86.29702807 247.62342311-209.57849677 247.62342311-1.59072863 0-3.31401797-0.53024288-4.90474659-1.06048576-57.00110917 117.8464792-141.70740862 197.11778917-239.93490141 212.36227186 0 0.53024288 0.53024288 1.06048575 0.53024286 1.59072864v141.70740861H685.6545419c19.4864257 0 34.73090838 15.77472557 34.73090839 35.26115127 0 19.4864257-15.77472557 35.26115125-34.73090839 35.26115125H337.0198509c-18.95618282 0-34.73090838-15.77472557-34.73090837-35.26115125 0-19.4864257 15.77472557-35.26115125 34.73090837-35.26115127h139.58643712V742.39052965c0-0.53024288 0-1.06048575 0.53024288-1.59072864-98.22749279-15.24448269-182.93379223-94.51579266-239.40465853-212.36227186-1.59072863 0-3.31401797 1.06048575-4.90474661 1.06048576-123.81171155 0.66280359-210.10873964-101.54151077-210.10873964-246.96061952 0-19.4864257 15.77472557-35.26115125 34.73090839-35.26115125h108.03698599c-1.59072863-24.3911723-3.31401797-48.91490531-3.31401797-73.83632049 0-21.20971504 1.06048575-43.47991584 2.7837751-69.46181677 1.59072863-18.42593995 16.30496844-32.60993687 34.73090838-32.60993687h622.77025792c18.42593995 0 33.67042263 14.05143621 34.73090838 32.60993687 2.1209715 25.58421877 2.65121438 48.38466243 2.65121438 69.46181677 0 24.92141517-1.06048575 49.97539106-3.31401798 73.83632049l108.69978958-0.66280361z m-149.79361248 210.24130036c60.31512715-11.40022183 102.60199651-62.96634153 112.41148973-139.05619424h-78.74106709c-7.55596098 49.44514819-18.95618282 95.57627841-33.67042264 139.05619424z m-304.0942894-24.52373302l59.78488426 30.88664754c20.14922928 10.33973609 33.14017975 0.53024288 29.29591891-21.20971505l-11.40022184-65.21987375 48.38466245-46.13113022c16.30496844-15.77472557 11.40022183-30.88664752-10.86997896-34.20066551L559.72185885 286.64677769l-29.82616178-59.1220807c-10.33973609-20.14922928-26.64470452-20.14922928-36.32163702 0L463.74789827 286.64677769l-66.81060238 9.80949319c-22.2702008 3.31401797-27.17494739 18.42593995-10.86997895 34.20066551l48.38466244 46.13113022-11.40022183 65.21987375c-3.84426085 22.2702008 9.27925033 31.54945113 29.2959189 21.20971505l58.98951996-30.88664754zM94.96397799 317.79854665c9.80949321 75.957292 52.09636257 127.65597241 112.41148972 139.05619424-14.71423981-42.94967296-26.11446164-89.61104606-33.67042262-139.05619424H94.96397799z m0 0'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconGongxian.displayName = 'icon-gongxian';
|
||||
|
||||
export default IconGongxian;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconMulu = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M874.642286 761.929143a51.785143 51.785143 0 1 1 0 103.570286H149.357714a51.785143 51.785143 0 1 1 0-103.570286h725.284572z m4.681143-386.267429a31.085714 31.085714 0 0 1 47.177142 26.697143v200.996572a31.085714 31.085714 0 0 1-47.177142 26.697142L711.826286 529.554286a31.085714 31.085714 0 0 1 0-53.394286l167.497143-100.498286zM512 450.998857a51.785143 51.785143 0 0 1 0 103.643429H149.357714a51.785143 51.785143 0 1 1 0-103.570286H512z m362.642286-310.857143a51.785143 51.785143 0 1 1 0 103.643429H149.357714a51.785143 51.785143 0 1 1 0-103.570286h725.284572z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconMulu.displayName = 'icon-mulu';
|
||||
|
||||
export default IconMulu;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconNeirongdagang = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M153.6 256h716.8a51.2 51.2 0 0 0 0-102.4H153.6a51.2 51.2 0 0 0 0 102.4z m512 204.8h-512a51.2 51.2 0 0 0 0 102.4h512a51.2 51.2 0 0 0 0-102.4z m-204.8 307.2H153.6a51.2 51.2 0 0 0 0 102.4h307.2a51.2 51.2 0 0 0 0-102.4z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconNeirongdagang.displayName = 'icon-neirongdagang';
|
||||
|
||||
export default IconNeirongdagang;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconWenzi = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path d='M563.2 281.6V870.4a51.2 51.2 0 0 1-102.4 0V281.6H179.2a51.2 51.2 0 1 1 0-102.4h665.6a51.2 51.2 0 0 1 0 102.4H563.2z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconWenzi.displayName = 'icon-wenzi';
|
||||
|
||||
export default IconWenzi;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconXinference = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M0 0m146.285714 0l731.428572 0q146.285714 0 146.285714 146.285714l0 731.428572q0 146.285714-146.285714 146.285714l-731.428572 0q-146.285714 0-146.285714-146.285714l0-731.428572q0-146.285714 146.285714-146.285714Z'
|
||||
fill='#9D62F7'
|
||||
></path>
|
||||
<path
|
||||
d='M413.793524 519.94819a315.489524 315.489524 0 0 0 59.294476 51.2 324.900571 324.900571 0 0 0 59.587048 31.451429 450.681905 450.681905 0 0 0 92.867047-124.611048l137.167238-271.603809-239.177143 188.025905a451.34019 451.34019 0 0 0-109.714285 125.561904z m-19.504762 169.081905q-25.161143-16.847238-48.761905-35.961905l-82.895238 164.547048 149.138286-117.248a1104.847238 1104.847238 0 0 1-17.578667-11.239619z'
|
||||
fill='#FFFFFF'
|
||||
></path>
|
||||
<path
|
||||
d='M701.854476 424.764952c44.958476 59.587048 58.075429 125.805714 27.599238 171.398096-44.495238 66.56-165.13219 64-269.433904-5.705143s-152.795429-180.150857-108.300191-246.735238c30.47619-45.592381 96.670476-58.758095 168.96-40.082286-125.001143-53.077333-243.809524-47.835429-290.133333 21.040762-57.880381 86.649905 21.162667 241.371429 176.542476 344.941714s328.289524 117.735619 386.194286 31.134476c46.153143-68.973714 5.36381-180.833524-91.428572-275.992381z'
|
||||
fill='#FFFFFF'
|
||||
></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconXinference.displayName = 'icon-Xinference';
|
||||
|
||||
export default IconXinference;
|
||||
|
|
@ -76,6 +76,7 @@ export { default as IconFeishujiqiren } from './IconFeishujiqiren';
|
|||
export { default as IconFenxi } from './IconFenxi';
|
||||
export { default as IconFireworks } from './IconFireworks';
|
||||
export { default as IconFuzhi } from './IconFuzhi';
|
||||
export { default as IconFuzhi1 } from './IconFuzhi1';
|
||||
export { default as IconGemini } from './IconGemini';
|
||||
export { default as IconGeminiAi } from './IconGeminiAi';
|
||||
export { default as IconGengduo } from './IconGengduo';
|
||||
|
|
@ -84,6 +85,7 @@ export { default as IconGitHub1 } from './IconGitHub1';
|
|||
export { default as IconGitee_ai } from './IconGitee_ai';
|
||||
export { default as IconGithub } from './IconGithub';
|
||||
export { default as IconGongjuTool } from './IconGongjuTool';
|
||||
export { default as IconGongxian } from './IconGongxian';
|
||||
export { default as IconGpustack } from './IconGpustack';
|
||||
export { default as IconGraphRag } from './IconGraphRag';
|
||||
export { default as IconGrok } from './IconGrok';
|
||||
|
|
@ -122,9 +124,11 @@ export { default as IconMistral } from './IconMistral';
|
|||
export { default as IconMixedbread } from './IconMixedbread';
|
||||
export { default as IconModaGPT } from './IconModaGPT';
|
||||
export { default as IconMoxing } from './IconMoxing';
|
||||
export { default as IconMulu } from './IconMulu';
|
||||
export { default as IconMulushouqi } from './IconMulushouqi';
|
||||
export { default as IconMuluwendang } from './IconMuluwendang';
|
||||
export { default as IconMuluzhankai } from './IconMuluzhankai';
|
||||
export { default as IconNeirongdagang } from './IconNeirongdagang';
|
||||
export { default as IconNeirongguanli } from './IconNeirongguanli';
|
||||
export { default as IconNeteaseYoudao } from './IconNeteaseYoudao';
|
||||
export { default as IconNewapi } from './IconNewapi';
|
||||
|
|
@ -187,6 +191,7 @@ export { default as IconWeixingongzhonghaoDaiyanse } from './IconWeixingongzhong
|
|||
export { default as IconWenjian } from './IconWenjian';
|
||||
export { default as IconWenjianjia } from './IconWenjianjia';
|
||||
export { default as IconWenjianjiaKai } from './IconWenjianjiaKai';
|
||||
export { default as IconWenzi } from './IconWenzi';
|
||||
export { default as IconWenzishuliang } from './IconWenzishuliang';
|
||||
export { default as IconWord } from './IconWord';
|
||||
export { default as IconXiajiantou } from './IconXiajiantou';
|
||||
|
|
@ -195,6 +200,7 @@ export { default as IconXiala1 } from './IconXiala1';
|
|||
export { default as IconXialaCopy } from './IconXialaCopy';
|
||||
export { default as IconXiaohongshu } from './IconXiaohongshu';
|
||||
export { default as IconXiaohongshuHui } from './IconXiaohongshuHui';
|
||||
export { default as IconXinference } from './IconXinference';
|
||||
export { default as IconYanzhengma } from './IconYanzhengma';
|
||||
export { default as IconYidongduan } from './IconYidongduan';
|
||||
export { default as IconYingweida } from './IconYingweida';
|
||||
|
|
|
|||
7554
web/pnpm-lock.yaml
7554
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue