kevisual-mark/src/manager/Manager.tsx
2025-03-29 23:16:44 +08:00

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>
);
};