generated from template/vite-react-template
359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
import { useManagerStore } from './store';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useShallow } from 'zustand/shallow';
|
|
import { ManagerProvider } from './Provider';
|
|
import { ChevronDown, ChevronLeft, X, Edit, Plus, Search, Trash, Menu as MenuIcon, MenuSquare } from 'lucide-react';
|
|
import dayjs from 'dayjs';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { EditMark as EditMarkComponent } from './edit/Edit';
|
|
import { toast } from 'react-toastify';
|
|
import clsx from 'clsx';
|
|
import { Controller, useForm } from 'react-hook-form';
|
|
import { Button, TextField, InputAdornment, IconButton, Menu, MenuItem } from '@mui/material';
|
|
import { MarkType } from '@kevisual/query-mark';
|
|
type ManagerProps = {
|
|
showSearch?: boolean;
|
|
showAdd?: boolean;
|
|
onClick?: (data?: any) => void;
|
|
markType?: MarkType;
|
|
showSelect?: boolean;
|
|
};
|
|
export const Manager = (props: ManagerProps) => {
|
|
const { showSearch = true, showAdd = false, onClick, showSelect = true } = props;
|
|
|
|
const { control } = useForm({ defaultValues: { search: '' } });
|
|
const { list, init, setCurrentMarkId, currentMarkId, markData, deleteMark, getMark, setMarkData, pagination, setPagination, getList, search, setSearch } =
|
|
useManagerStore(
|
|
useShallow((state) => {
|
|
return {
|
|
list: state.list,
|
|
init: state.init,
|
|
markData: state.markData,
|
|
currentMarkId: state.currentMarkId,
|
|
setCurrentMarkId: state.setCurrentMarkId,
|
|
deleteMark: state.deleteMark,
|
|
getMark: state.getMark,
|
|
setMarkData: state.setMarkData,
|
|
pagination: state.pagination,
|
|
setPagination: state.setPagination,
|
|
search: state.search,
|
|
setSearch: state.setSearch,
|
|
getList: state.getList,
|
|
};
|
|
}),
|
|
);
|
|
const { t } = useTranslation();
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
const open = Boolean(anchorEl);
|
|
|
|
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
const handleMenuItemClick = (option: string) => {
|
|
handleClose();
|
|
console.log('option', option);
|
|
init(option as any);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const url = new URL(window.location.href);
|
|
let markType = url.searchParams.get('markType') || '';
|
|
if (!markType && props.markType) {
|
|
markType = props.markType;
|
|
}
|
|
init((markType as any) || 'md');
|
|
}, []);
|
|
useEffect(() => {
|
|
if (search) {
|
|
getList();
|
|
} else if (pagination.current > 1) {
|
|
getList();
|
|
}
|
|
}, [pagination.current, search]);
|
|
const onEditMark = async (markId: string) => {
|
|
setCurrentMarkId(markId);
|
|
const res = await getMark(markId);
|
|
console.log('mark', res);
|
|
if (res.code === 200) {
|
|
setMarkData(res.data!);
|
|
}
|
|
};
|
|
const onDeleteMark = async (markId: string) => {
|
|
const res = await deleteMark(markId);
|
|
if (res.code === 200) {
|
|
toast.success(t('deleteMarkSuccess'));
|
|
}
|
|
};
|
|
console.log('list', list.length, pagination.total);
|
|
|
|
return (
|
|
<div className='w-full h-full p-4 bg-white border-r border-r-gray-200 relative'>
|
|
<div className='flex px-4 mb-4 justify-between items-center absolute top-0 left-0 h-[56px] w-full'>
|
|
<div className='flex ml-12 items-center space-x-2 '>
|
|
<Controller
|
|
name='search'
|
|
control={control}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
variant='outlined'
|
|
margin='normal'
|
|
sx={{
|
|
display: showSearch ? 'block' : 'none',
|
|
}}
|
|
size='small'
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: (
|
|
<InputAdornment position='end'>
|
|
<Search className='w-4 h-4' onClick={() => setSearch(field.value)} />
|
|
</InputAdornment>
|
|
),
|
|
onKeyDown: (event) => {
|
|
if (event.key === 'Enter') {
|
|
setSearch(field.value);
|
|
}
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className={'flex items-center space-x-2'}>
|
|
{showSelect && (
|
|
<>
|
|
<IconButton onClick={handleClick}>
|
|
<MenuIcon className='w-4 h-4' />
|
|
</IconButton>
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
open={open}
|
|
onClose={handleClose}
|
|
onClick={(e) => {
|
|
console.log('e', e);
|
|
}}>
|
|
{['md', 'mdx', 'wallnote', 'excalidraw'].map((option) => (
|
|
<MenuItem
|
|
key={option}
|
|
value={option}
|
|
onClick={() => {
|
|
handleMenuItemClick(option);
|
|
}}>
|
|
{option}
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
</>
|
|
)}
|
|
<button
|
|
className={clsx(
|
|
'text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200',
|
|
showAdd ? '' : 'hidden',
|
|
)}>
|
|
<Plus
|
|
className={clsx('w-4 h-4 ')}
|
|
onClick={() => {
|
|
setCurrentMarkId('');
|
|
|
|
setMarkData({
|
|
id: '',
|
|
title: '',
|
|
description: '',
|
|
markType: props.markType || ('md' as any),
|
|
summary: '',
|
|
tags: [],
|
|
link: '',
|
|
});
|
|
}}
|
|
/>
|
|
</button>
|
|
{markData && (
|
|
<button
|
|
className='text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200'
|
|
onClick={() => {
|
|
setCurrentMarkId('');
|
|
setMarkData(undefined);
|
|
}}>
|
|
<X className='w-4 h-4 ' />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className='mt-[56px] overflow-auto scrollbar' style={{ height: 'calc(100% - 56px)' }}>
|
|
{list.map((item, index) => {
|
|
const isCurrent = item.id === currentMarkId;
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className={`border rounded-lg p-4 mb-4 shadow-md bg-white border-gray-200 ${isCurrent ? 'border-blue-500' : ''}`}
|
|
onClick={() => {
|
|
onClick?.(item);
|
|
}}>
|
|
<div className='flex justify-between items-center'>
|
|
<div className={`text-lg font-bold truncate cursor-pointer ${isCurrent ? 'text-blue-500' : ''}`}>{item.title}</div>
|
|
<div className='flex space-x-2'>
|
|
<button
|
|
className='text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200'
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onEditMark(item.id);
|
|
}}>
|
|
<Edit className='w-4 h-4 ' />
|
|
</button>
|
|
<button
|
|
className='text-red-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-red-100 transition duration-200'
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDeleteMark(item.id);
|
|
}}>
|
|
<Trash className='w-4 h-4 ' />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className='text-sm text-gray-600'>
|
|
{t('markType')}: {item.markType}
|
|
</div>
|
|
<div className='text-sm text-gray-600'>
|
|
{t('summary')}: {item.summary}
|
|
</div>
|
|
<div className='text-sm text-gray-600'>
|
|
{t('tags')}: {item.tags?.join?.(', ')}
|
|
</div>
|
|
<div className='text-sm text-gray-600 hidden sm:block'>
|
|
{t('description')}: {item.description}
|
|
</div>
|
|
<div
|
|
className='text-sm text-gray-600 hidden sm:block truncate'
|
|
onClick={() => {
|
|
window.open(item.link, '_blank');
|
|
}}>
|
|
{t('link')}: {item.link}
|
|
</div>
|
|
<div className='text-sm text-gray-600 hidden sm:block'>
|
|
{t('createdAt')}: {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
</div>
|
|
<div className='text-sm text-gray-600 hidden sm:block'>
|
|
{t('updatedAt')}: {dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
<div className='flex justify-center items-center'>
|
|
{list.length < pagination.total && (
|
|
<button
|
|
className='text-blue-500 cursor-pointer hover:underline flex items-center p-2 rounded-md hover:bg-blue-100 transition duration-200'
|
|
onClick={() => {
|
|
setPagination({ ...pagination, current: pagination.current + 1 });
|
|
}}>
|
|
<ChevronDown className='w-4 h-4 ' />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const EditMark = () => {
|
|
const { markData } = useManagerStore(
|
|
useShallow((state) => {
|
|
return {
|
|
markData: state.markData,
|
|
};
|
|
}),
|
|
);
|
|
const mark = markData;
|
|
if (!mark) {
|
|
return null;
|
|
}
|
|
if (mark) {
|
|
return <EditMarkComponent />;
|
|
}
|
|
return <div className='w-full h-full'></div>;
|
|
};
|
|
export const LayoutMain = (props: { children?: React.ReactNode; expandChildren?: React.ReactNode }) => {
|
|
const [openMenu, setOpenMenu] = useState(false);
|
|
const getDocumentHeight = () => {
|
|
return document.documentElement.scrollHeight;
|
|
};
|
|
const markData = useManagerStore((state) => state.markData);
|
|
const isEdit = !!markData;
|
|
const hasExpandChildren = !!props.expandChildren;
|
|
const style = useMemo(() => {
|
|
if (!hasExpandChildren || openMenu) {
|
|
return {};
|
|
}
|
|
return {
|
|
top: getDocumentHeight() / 2 + 10,
|
|
};
|
|
}, [getDocumentHeight, hasExpandChildren, openMenu]);
|
|
return (
|
|
<div className='w-full h-full flex'>
|
|
<div className={clsx('absolute top-4 z-10', openMenu ? 'left-4' : '-left-4')} style={style}>
|
|
<Button
|
|
variant='contained'
|
|
color={openMenu ? 'info' : 'primary'}
|
|
sx={{
|
|
minWidth: '0px',
|
|
padding: '8px',
|
|
}}
|
|
onClick={() => {
|
|
setOpenMenu(!openMenu);
|
|
}}>
|
|
<MenuSquare className='w-4 h-4' />
|
|
</Button>
|
|
</div>
|
|
<div className={clsx('h-full w-full sm:w-1/3', openMenu ? '' : 'hidden')}>{props.children}</div>
|
|
{(!props.expandChildren || isEdit) && (
|
|
<div className={clsx('h-full hidden sm:block sm:w-2/3', openMenu ? '' : 'hidden')}>
|
|
<EditMark />
|
|
</div>
|
|
)}
|
|
{props.expandChildren && <div className='h-full grow'>{props.expandChildren}</div>}
|
|
</div>
|
|
);
|
|
};
|
|
export type AppProps = {
|
|
/**
|
|
* 标记类型, wallnote md excalidraw
|
|
*/
|
|
markType?: MarkType;
|
|
/**
|
|
* 是否显示搜索框
|
|
*/
|
|
showSearch?: boolean;
|
|
/**
|
|
* 是否显示添加按钮
|
|
*/
|
|
showAdd?: boolean;
|
|
/**
|
|
* 点击事件
|
|
*/
|
|
onClick?: (data?: any) => void;
|
|
/**
|
|
* 管理器id, 存储到store的id
|
|
*/
|
|
managerId?: string;
|
|
children?: React.ReactNode;
|
|
showSelect?: boolean;
|
|
};
|
|
export const App = (props: AppProps) => {
|
|
return (
|
|
<ManagerProvider id={props.managerId}>
|
|
<LayoutMain expandChildren={props.children}>
|
|
<Manager
|
|
markType={props.markType}
|
|
showSearch={props.showSearch}
|
|
showAdd={props.showAdd}
|
|
onClick={props.onClick}
|
|
showSelect={props.showSelect}></Manager>
|
|
</LayoutMain>
|
|
</ManagerProvider>
|
|
);
|
|
};
|