generated from template/vite-react-template
mark模块更新
This commit is contained in:
parent
9129dffa4c
commit
508ec96029
19
index.html
19
index.html
@ -1,13 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Mark</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
18
package.json
18
package.json
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "vite-react",
|
||||
"name": "@kevisual/mark",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
@ -9,14 +9,17 @@
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"pub": "envision deploy ./dist -k vite-react -v 0.0.1",
|
||||
"ev": "npm run build && npm run deploy",
|
||||
"pub": "envision deploy ./dist -k mark -v 0.0.1",
|
||||
"ev": "npm run build && npm run pub",
|
||||
"dev:lib": "turbo dev"
|
||||
},
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kevisual/query-mark": "workspace:*",
|
||||
"@kevisual/router": "0.0.9",
|
||||
"@kevisual/store": "workspace:*",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash-es": "^4.17.21",
|
||||
@ -24,20 +27,21 @@
|
||||
"nanoid": "^5.1.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-toastify": "^11.0.5",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/query": "0.0.15",
|
||||
"@kevisual/types": "^0.0.6",
|
||||
"@tailwindcss/vite": "^4.0.16",
|
||||
"@types/node": "^22.13.13",
|
||||
"@tailwindcss/vite": "^4.0.17",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.0.16",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
"packageManager": "pnpm@10.7.0"
|
||||
}
|
13
public/locales/en/translation.json
Normal file
13
public/locales/en/translation.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"markType": "Type",
|
||||
"summary": "Summary",
|
||||
"tags": "Tags",
|
||||
"description": "Description",
|
||||
"link": "Link",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"title": "Title",
|
||||
"thumbnail": "Thumbnail",
|
||||
"save": "Save",
|
||||
"editMarkSuccess": "Edit Mark Success"
|
||||
}
|
13
public/locales/zh/translation.json
Normal file
13
public/locales/zh/translation.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"markType": "类型",
|
||||
"summary": "摘要",
|
||||
"tags": "标签",
|
||||
"description": "描述",
|
||||
"link": "链接",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"title": "标题",
|
||||
"thumbnail": "缩略图",
|
||||
"save": "保存",
|
||||
"editMarkSuccess": "编辑成功"
|
||||
}
|
16
src/MarkProvider.tsx
Normal file
16
src/MarkProvider.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { initI18n, I18NextProvider } from '@kevisual/components/translate/index.tsx';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { basename } from './modules/basename';
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
basename?: string;
|
||||
};
|
||||
// initI18n('');
|
||||
export const MarkProvider = (props: Props) => {
|
||||
return (
|
||||
<I18NextProvider basename={props.basename || basename} noUse={false}>
|
||||
<ToastContainer />
|
||||
{props.children}
|
||||
</I18NextProvider>
|
||||
);
|
||||
};
|
@ -1 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
@import '@kevisual/components/theme/wind-theme.css';
|
||||
|
@ -3,4 +3,9 @@ import { App } from './pages/App.tsx';
|
||||
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
import { MarkProvider } from './MarkProvider.tsx';
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<MarkProvider>
|
||||
<App />
|
||||
</MarkProvider>,
|
||||
);
|
||||
|
317
src/manager/Manager.tsx
Normal file
317
src/manager/Manager.tsx
Normal file
@ -0,0 +1,317 @@
|
||||
import { useManagerStore } from './store';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { ManagerProvider } from './Provider';
|
||||
import { ChevronDown, ChevronLeft, 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;
|
||||
};
|
||||
export const Manager = (props: ManagerProps) => {
|
||||
const { showSearch = true, showAdd = false, onClick } = props;
|
||||
|
||||
const { control } = useForm({ defaultValues: { search: '' } });
|
||||
const { list, init, setCurrentMarkId, currentMarkId, deleteMark, getMark, setMarkData, pagination, setPagination, getList, search, setSearch } =
|
||||
useManagerStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
list: state.list,
|
||||
init: state.init,
|
||||
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 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'}>
|
||||
<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 ', currentMarkId ? 'rotate-12' : 'rotate-0')}
|
||||
onClick={() => {
|
||||
setCurrentMarkId('');
|
||||
|
||||
setMarkData({
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
markType: 'md' as any,
|
||||
summary: '',
|
||||
tags: [],
|
||||
link: '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</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 }) => {
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
return (
|
||||
<div className='w-full h-full flex'>
|
||||
<div className='absolute top-4 left-4 z-10'>
|
||||
<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>
|
||||
<div className={clsx('h-full hidden sm:block sm:w-2/3', openMenu ? '' : 'hidden')}>
|
||||
<EditMark />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export type AppProps = {
|
||||
/**
|
||||
* 标记类型, wallnote md excalidraw
|
||||
*/
|
||||
markType?: MarkType;
|
||||
/**
|
||||
* 是否显示搜索框
|
||||
*/
|
||||
showSearch?: boolean;
|
||||
/**
|
||||
* 是否显示添加按钮
|
||||
*/
|
||||
showAdd?: boolean;
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
onClick?: (data?: any) => void;
|
||||
/**
|
||||
* 管理器id, 存储到store的id
|
||||
*/
|
||||
managerId?: string;
|
||||
};
|
||||
export const App = (props: AppProps) => {
|
||||
return (
|
||||
<ManagerProvider id={props.managerId}>
|
||||
<LayoutMain>
|
||||
<Manager markType={props.markType} showSearch={props.showSearch} showAdd={props.showAdd} onClick={props.onClick} />
|
||||
</LayoutMain>
|
||||
</ManagerProvider>
|
||||
);
|
||||
};
|
9
src/manager/Provider.tsx
Normal file
9
src/manager/Provider.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { StoreContextProvider } from '@kevisual/store/react';
|
||||
import { createManagerStore } from './store/index';
|
||||
export const ManagerProvider = ({ children, id }: { children: React.ReactNode; id?: string }) => {
|
||||
return (
|
||||
<StoreContextProvider id={id || 'mark-manager'} stateCreator={createManagerStore}>
|
||||
{children}
|
||||
</StoreContextProvider>
|
||||
);
|
||||
};
|
99
src/manager/edit/Edit.tsx
Normal file
99
src/manager/edit/Edit.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { TextField, Button, Box, MenuItem, Autocomplete, FormControlLabel } from '@mui/material';
|
||||
import { useManagerStore } from '../store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Mark } from '@kevisual/query-mark';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { pick } from 'lodash-es';
|
||||
import { toast } from 'react-toastify';
|
||||
import { TagsInput } from '@kevisual/components/select/TagsInput.tsx';
|
||||
export const EditMark = () => {
|
||||
const { control, handleSubmit, reset } = useForm();
|
||||
const { updateMark, markData, setCurrentMarkId, setMarkData } = useManagerStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
updateMark: state.updateMark,
|
||||
markData: state.markData,
|
||||
setCurrentMarkId: state.setCurrentMarkId,
|
||||
currentMarkId: state.currentMarkId,
|
||||
setMarkData: state.setMarkData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
// const [mark, setMark] = useState<Mark | undefined>(markData);
|
||||
const mark = pick(markData, ['id', 'title', 'description', 'markType', 'summary', 'tags', 'link', 'thumbnail']);
|
||||
useEffect(() => {
|
||||
reset(mark);
|
||||
console.log('markData', markData);
|
||||
}, [markData?.id]);
|
||||
const onSubmit = async (data: any) => {
|
||||
const res = await updateMark({ ...mark, ...data });
|
||||
if (res.code === 200) {
|
||||
toast.success(t('editMarkSuccess'));
|
||||
}
|
||||
|
||||
// setCurrentMarkId('');
|
||||
// setMarkData(undefined);
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box component='form' sx={{ px: 2, py: 1 }} onSubmit={handleSubmit(onSubmit)} noValidate autoComplete='off' className='w-full h-full overflow-auto'>
|
||||
<Controller
|
||||
name='title'
|
||||
control={control}
|
||||
defaultValue={mark?.title || ''}
|
||||
render={({ field }) => <TextField {...field} label={t('title')} variant='outlined' fullWidth margin='normal' />}
|
||||
/>
|
||||
<Controller
|
||||
name='description'
|
||||
control={control}
|
||||
defaultValue={mark?.description || ''}
|
||||
render={({ field }) => <TextField {...field} label={t('description')} variant='outlined' fullWidth margin='normal' multiline />}
|
||||
/>
|
||||
<Controller
|
||||
name='markType'
|
||||
control={control}
|
||||
defaultValue={mark?.markType || ''}
|
||||
render={({ field }) => (
|
||||
<Autocomplete
|
||||
{...field}
|
||||
options={['md', 'mdx', 'wallnote', 'excalidraw']}
|
||||
freeSolo
|
||||
renderInput={(params) => <TextField {...params} label={t('markType')} variant='outlined' fullWidth margin='normal' />}
|
||||
onChange={(_, value) => field.onChange(value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name='summary'
|
||||
control={control}
|
||||
defaultValue={mark?.summary || ''}
|
||||
render={({ field }) => <TextField {...field} label={t('summary')} variant='outlined' fullWidth margin='normal' multiline />}
|
||||
/>
|
||||
<Controller
|
||||
name='tags'
|
||||
control={control}
|
||||
defaultValue={mark?.tags || ''}
|
||||
render={({ field }) => {
|
||||
return <TagsInput {...field} label={t('tags')} showLabel={true} />;
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name='link'
|
||||
control={control}
|
||||
defaultValue={mark?.link || ''}
|
||||
render={({ field }) => <TextField {...field} label={t('link')} variant='outlined' fullWidth margin='normal' />}
|
||||
/>
|
||||
<Controller
|
||||
name='thumbnail'
|
||||
control={control}
|
||||
defaultValue={mark?.thumbnail || ''}
|
||||
render={({ field }) => <TextField {...field} label={t('thumbnail')} variant='outlined' fullWidth margin='normal' />}
|
||||
/>
|
||||
<Button type='submit' variant='contained' color='primary'>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
133
src/manager/store/index.ts
Normal file
133
src/manager/store/index.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { StateCreator, StoreManager } from '@kevisual/store';
|
||||
import { useContextKey } from '@kevisual/store/context';
|
||||
// import { StateCreator, StoreApi, UseBoundStore } from 'zustand';
|
||||
import { query as queryClient } from '../../modules/query';
|
||||
import { Result } from '@kevisual/query/query';
|
||||
import { QueryMark, Mark, MarkType } from '@kevisual/query-mark';
|
||||
import { useStore, BoundStore } from '@kevisual/store/react';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
export const store = useContextKey('store', () => {
|
||||
return new StoreManager();
|
||||
});
|
||||
|
||||
type ManagerStore = {
|
||||
/** 当前选中的Mark */
|
||||
currentMarkId: string;
|
||||
setCurrentMarkId: (markId: string) => void;
|
||||
markData: Mark | undefined;
|
||||
setMarkData: (mark?: Partial<Mark>) => void;
|
||||
/** 获取Mark列表 */
|
||||
getList: () => Promise<any>;
|
||||
getMarkFromList: (markId: string) => Mark | undefined;
|
||||
updateMark: (mark: Mark) => Promise<any>;
|
||||
getMark: (markId: string) => Promise<Result<Mark>>;
|
||||
deleteMark: (markId: string) => Promise<any>;
|
||||
/** Mark列表 */
|
||||
list: Mark[];
|
||||
setList: (list: Mark[]) => void;
|
||||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
setPagination: (pagination: { current: number; pageSize: number; total: number }) => void;
|
||||
/** 搜索 */
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
/** 初始化 */
|
||||
init: (markType: MarkType) => Promise<void>;
|
||||
queryMark: QueryMark;
|
||||
markType: MarkType;
|
||||
};
|
||||
export const createManagerStore: StateCreator<ManagerStore, [], [], any> = (set, get, store) => {
|
||||
return {
|
||||
currentMarkId: '',
|
||||
setCurrentMarkId: (markId: string) => set(() => ({ currentMarkId: markId })),
|
||||
getList: async () => {
|
||||
const queryMark = get().queryMark;
|
||||
const { search, pagination } = get();
|
||||
const res = await queryMark.getMarkList({ page: pagination.current, pageSize: pagination.pageSize, search });
|
||||
const oldList = get().list;
|
||||
if (res.code === 200) {
|
||||
const { pagination, list } = res.data || {};
|
||||
const newList = [...oldList, ...list];
|
||||
const uniqueList = uniqBy(newList, 'id');
|
||||
set(() => ({ list: uniqueList }));
|
||||
set(() => ({ pagination: { current: pagination.current, pageSize: pagination.pageSize, total: pagination.total } }));
|
||||
}
|
||||
},
|
||||
getMarkFromList: (markId: string) => {
|
||||
return get().list.find((item) => item.id === markId);
|
||||
},
|
||||
updateMark: async (mark: Mark) => {
|
||||
const queryMark = get().queryMark;
|
||||
const res = await queryMark.updateMark(mark.id, mark);
|
||||
if (res.code === 200) {
|
||||
set((state) => {
|
||||
const oldList = state.list;
|
||||
const resMark = res.data!;
|
||||
const newList = oldList.map((item) => (item.id === mark.id ? mark : item));
|
||||
if (!mark.id) {
|
||||
newList.unshift(resMark);
|
||||
}
|
||||
return {
|
||||
list: newList,
|
||||
};
|
||||
});
|
||||
}
|
||||
return res;
|
||||
},
|
||||
getMark: async (markId: string) => {
|
||||
const queryMark = get().queryMark;
|
||||
const res = await queryMark.getMark(markId);
|
||||
return res;
|
||||
},
|
||||
list: [],
|
||||
setList: (list: any[]) => set(() => ({ list })),
|
||||
init: async (markType: MarkType = 'wallnote') => {
|
||||
// await get().getList();
|
||||
console.log('init', set, get);
|
||||
const queryMark = new QueryMark({
|
||||
query: queryClient as any,
|
||||
markType,
|
||||
});
|
||||
const url = new URL(window.location.href);
|
||||
const pageSize = url.searchParams.get('pageSize') || '10';
|
||||
set({ queryMark, markType, list: [], pagination: { current: 1, pageSize: parseInt(pageSize), total: 0 }, currentMarkId: '', markData: undefined });
|
||||
setTimeout(async () => {
|
||||
console.log('get', get);
|
||||
get().getList();
|
||||
}, 1000);
|
||||
},
|
||||
deleteMark: async (markId: string) => {
|
||||
const queryMark = get().queryMark;
|
||||
const res = await queryMark.deleteMark(markId);
|
||||
const currentMarkId = get().currentMarkId;
|
||||
if (res.code === 200) {
|
||||
// get().getList();
|
||||
set((state) => ({
|
||||
list: state.list.filter((item) => item.id !== markId),
|
||||
}));
|
||||
if (currentMarkId === markId) {
|
||||
set(() => ({ currentMarkId: '', markData: undefined }));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
queryMark: undefined,
|
||||
markType: 'simple',
|
||||
markData: undefined,
|
||||
setMarkData: (mark: Mark) => set(() => ({ markData: mark })),
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
},
|
||||
setPagination: (pagination: { current: number; pageSize: number; total: number }) => set(() => ({ pagination })),
|
||||
/** 搜索 */
|
||||
search: '',
|
||||
setSearch: (search: string) => set(() => ({ search, list: [], pagination: { current: 1, pageSize: 10, total: 0 } })),
|
||||
};
|
||||
};
|
||||
|
||||
export const useManagerStore = useStore as BoundStore<ManagerStore>;
|
@ -1,5 +1,10 @@
|
||||
import { basename } from '../modules/basename';
|
||||
console.log('basename', basename);
|
||||
import { App as MarkApp } from '../manager/Manager';
|
||||
export const App = () => {
|
||||
return <div className='bg-slate-200 w-full h-full border'></div>;
|
||||
return (
|
||||
<div className=' w-full h-full overflow-hidden'>
|
||||
<MarkApp />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -10,6 +10,17 @@ const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const basename = isDev ? '/' : pkgs?.basename || '/';
|
||||
|
||||
let proxy: any = {};
|
||||
if (isDev) {
|
||||
proxy = {
|
||||
'/api': {
|
||||
target: 'https://kevisual.xiongxiao.me',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewriteWsOrigin: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
@ -38,13 +49,7 @@ export default defineConfig({
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/router': {
|
||||
target: 'ws://localhost:3000',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
...proxy,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user