generated from template/vite-react-template
mark模块更新
This commit is contained in:
parent
9129dffa4c
commit
508ec96029
27
index.html
27
index.html
@ -1,13 +1,30 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Mark</title>
|
||||||
</head>
|
<style>
|
||||||
<body>
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
18
package.json
18
package.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react",
|
"name": "@kevisual/mark",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -9,14 +9,17 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"pub": "envision deploy ./dist -k vite-react -v 0.0.1",
|
"pub": "envision deploy ./dist -k mark -v 0.0.1",
|
||||||
"ev": "npm run build && npm run deploy",
|
"ev": "npm run build && npm run pub",
|
||||||
"dev:lib": "turbo dev"
|
"dev:lib": "turbo dev"
|
||||||
},
|
},
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@kevisual/query-mark": "workspace:*",
|
||||||
"@kevisual/router": "0.0.9",
|
"@kevisual/router": "0.0.9",
|
||||||
|
"@kevisual/store": "workspace:*",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@ -24,20 +27,21 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/query": "0.0.15",
|
"@kevisual/query": "0.0.15",
|
||||||
"@kevisual/types": "^0.0.6",
|
"@kevisual/types": "^0.0.6",
|
||||||
"@tailwindcss/vite": "^4.0.16",
|
"@tailwindcss/vite": "^4.0.17",
|
||||||
"@types/node": "^22.13.13",
|
"@types/node": "^22.13.14",
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"tailwindcss": "^4.0.16",
|
"tailwindcss": "^4.0.17",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite": "^6.2.3"
|
"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';
|
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';
|
import { basename } from '../modules/basename';
|
||||||
console.log('basename', basename);
|
console.log('basename', basename);
|
||||||
|
import { App as MarkApp } from '../manager/Manager';
|
||||||
export const App = () => {
|
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 || '/';
|
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/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
@ -38,13 +49,7 @@ export default defineConfig({
|
|||||||
rewriteWsOrigin: true,
|
rewriteWsOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||||
},
|
},
|
||||||
'/api/router': {
|
...proxy,
|
||||||
target: 'ws://localhost:3000',
|
|
||||||
changeOrigin: true,
|
|
||||||
ws: true,
|
|
||||||
rewriteWsOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user