Files
draw/src/pages/mark/manager/Manager.tsx
2026-02-14 02:31:51 +08:00

332 lines
11 KiB
TypeScript

import { useManagerStore } from './store';
import { useEffect, useMemo, useState } from 'react';
import { useShallow } from 'zustand/shallow';
import { ChevronDown, X, Edit, Plus, Search, Trash, Menu as MenuIcon, MenuSquare } from 'lucide-react';
import dayjs from 'dayjs';
import { EditMark as EditMarkComponent } from './edit/Edit';
import { toast } from 'sonner';
import clsx from 'clsx';
import { Controller, useForm } from 'react-hook-form';
import { IconButton } from '@/components/a/button';
import { MarkType } from '@kevisual/api/query-mark';
import { Menu } from '@/components/a/menu';
import { MarkTypes } from './constant';
type ManagerProps = {
showSearch?: boolean;
showAdd?: boolean;
onClick?: (data?: any, e?: Event) => void;
markType?: MarkType;
showSelect?: boolean;
};
export { useManagerStore };
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 handleMenuItemClick = (option: string) => {
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('删除成功');
}
};
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 }) => (
<div className={`relative ${showSearch ? 'block' : 'hidden'}`}>
<input
{...field}
type='text'
className='py-2 px-3 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
onKeyDown={(event) => {
if (event.key === 'Enter') {
setSearch(field.value);
if (!field.value) {
getList();
}
}
}}
/>
<div className='absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer'>
<Search className='w-4 h-4' onClick={() => setSearch(field.value)} />
</div>
</div>
)}
/>
</div>
<div className={'flex items-center space-x-2'}>
{showSelect && (
<>
<Menu
options={MarkTypes.map((item) => {
return { label: item, value: item };
})}
onSelect={handleMenuItemClick}>
<MenuIcon className='w-4 h-4' />
</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={(e) => {
onClick?.(item, e as any);
e.stopPropagation();
e.preventDefault();
}}>
<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'>: {item.markType}</div>
<div className='text-sm text-gray-600'>: {item.summary}</div>
<div className='text-sm text-gray-600'>: {item.tags?.join?.(', ')}</div>
{/* <div className='text-sm text-gray-600 hidden sm:block'>描述: {item.description}</div> */}
<div
className='text-sm text-gray-600 hidden sm:block truncate'
onClick={() => {
window.open(item.link, '_blank');
}}>
: {item.link}
</div>
<div className='text-sm text-gray-600 hidden sm:block'>: {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}</div>
<div className='text-sm text-gray-600 hidden sm:block'>: {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; open?: boolean; hasTopTitle?: boolean }) => {
const getDocumentHeight = () => {
return document.documentElement.scrollHeight;
};
const mStore = useManagerStore(
useShallow((state) => {
return {
open: state.open,
setOpen: state.setOpen,
markData: state.markData,
};
}),
);
const markData = mStore.markData;
const openMenu = mStore.open;
const setOpenMenu = mStore.setOpen;
useEffect(() => {
if (props.open !== undefined) {
setOpenMenu!(props.open);
}
}, []);
const isEdit = !!markData;
const hasExpandChildren = !!props.expandChildren;
const style = useMemo(() => {
const top = props.hasTopTitle ? 70 : 0; // Adjust top based on whether there's a title
if (!hasExpandChildren || openMenu) {
return {
top,
};
}
return {
top: getDocumentHeight() / 2 + 10 + top,
};
}, [getDocumentHeight, hasExpandChildren, openMenu, props.hasTopTitle]);
return (
<div className='w-full h-full flex'>
<div className={clsx('absolute top-4 z-10', openMenu ? 'left-4' : '-left-4')} style={style}>
<IconButton
onClick={() => {
setOpenMenu(!openMenu);
}}>
<MenuSquare className='w-4 h-4' />
</IconButton>
</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;
openMenu?: boolean;
hasTopTitle?: boolean; // 是否有顶部标题
};
export const ProviderManagerName = 'mark-manager';
export const App = (props: AppProps) => {
return (
<LayoutMain expandChildren={props.children} open={props.openMenu} hasTopTitle={props.hasTopTitle}>
<Manager
markType={props.markType}
showSearch={props.showSearch}
showAdd={props.showAdd}
onClick={props.onClick}
showSelect={props.showSelect}></Manager>
</LayoutMain>
);
};