diff --git a/README.md b/README.md index 95e2957..371e667 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# vite-react-template \ No newline at end of file +# mark 管理界面的模块功能 \ No newline at end of file diff --git a/index.html b/index.html index e4b78ea..a36ee5c 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,30 @@ - - - - - Vite + React + TS - - -
- - - + + + + + + Mark + + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index b11320f..f6d800b 100644 --- a/package.json +++ b/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 ", "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" } \ No newline at end of file diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000..a7b32be --- /dev/null +++ b/public/locales/en/translation.json @@ -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" +} \ No newline at end of file diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json new file mode 100644 index 0000000..13344e0 --- /dev/null +++ b/public/locales/zh/translation.json @@ -0,0 +1,13 @@ +{ + "markType": "类型", + "summary": "摘要", + "tags": "标签", + "description": "描述", + "link": "链接", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "title": "标题", + "thumbnail": "缩略图", + "save": "保存", + "editMarkSuccess": "编辑成功" +} \ No newline at end of file diff --git a/src/MarkProvider.tsx b/src/MarkProvider.tsx new file mode 100644 index 0000000..b14ea0c --- /dev/null +++ b/src/MarkProvider.tsx @@ -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 ( + + + {props.children} + + ); +}; diff --git a/src/index.css b/src/index.css index f1d8c73..d71bc93 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,2 @@ -@import "tailwindcss"; +@import 'tailwindcss'; +@import '@kevisual/components/theme/wind-theme.css'; diff --git a/src/main.tsx b/src/main.tsx index 7cf35ea..9552924 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,4 +3,9 @@ import { App } from './pages/App.tsx'; import './index.css'; -createRoot(document.getElementById('root')!).render(); +import { MarkProvider } from './MarkProvider.tsx'; +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/manager/Manager.tsx b/src/manager/Manager.tsx new file mode 100644 index 0000000..67c8f6c --- /dev/null +++ b/src/manager/Manager.tsx @@ -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); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + 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 ( +
+
+
+ ( + + setSearch(field.value)} /> + + ), + onKeyDown: (event) => { + if (event.key === 'Enter') { + setSearch(field.value); + } + }, + }, + }} + /> + )} + /> +
+
+ + + + { + console.log('e', e); + }}> + {['md', 'mdx', 'wallnote', 'excalidraw'].map((option) => ( + { + handleMenuItemClick(option); + }}> + {option} + + ))} + + +
+
+
+ {list.map((item, index) => { + const isCurrent = item.id === currentMarkId; + return ( +
{ + onClick?.(item); + }}> +
+
{item.title}
+
+ + +
+
+
+ {t('markType')}: {item.markType} +
+
+ {t('summary')}: {item.summary} +
+
+ {t('tags')}: {item.tags?.join?.(', ')} +
+
+ {t('description')}: {item.description} +
+
{ + window.open(item.link, '_blank'); + }}> + {t('link')}: {item.link} +
+
+ {t('createdAt')}: {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')} +
+
+ {t('updatedAt')}: {dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss')} +
+
+ ); + })} +
+ {list.length < pagination.total && ( + + )} +
+
+
+ ); +}; + +export const EditMark = () => { + const { markData } = useManagerStore( + useShallow((state) => { + return { + markData: state.markData, + }; + }), + ); + const mark = markData; + if (!mark) { + return null; + } + if (mark) { + return ; + } + return
; +}; +export const LayoutMain = (props: { children?: React.ReactNode }) => { + const [openMenu, setOpenMenu] = useState(false); + return ( +
+
+ +
+
{props.children}
+ +
+ ); +}; +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 ( + + + + + + ); +}; diff --git a/src/manager/Provider.tsx b/src/manager/Provider.tsx new file mode 100644 index 0000000..a5a074e --- /dev/null +++ b/src/manager/Provider.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/src/manager/edit/Edit.tsx b/src/manager/edit/Edit.tsx new file mode 100644 index 0000000..627701b --- /dev/null +++ b/src/manager/edit/Edit.tsx @@ -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(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 ( + + } + /> + } + /> + ( + } + onChange={(_, value) => field.onChange(value)} + /> + )} + /> + } + /> + { + return ; + }} + /> + } + /> + } + /> + + + ); +}; diff --git a/src/manager/store/index.ts b/src/manager/store/index.ts new file mode 100644 index 0000000..3957827 --- /dev/null +++ b/src/manager/store/index.ts @@ -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) => void; + /** 获取Mark列表 */ + getList: () => Promise; + getMarkFromList: (markId: string) => Mark | undefined; + updateMark: (mark: Mark) => Promise; + getMark: (markId: string) => Promise>; + deleteMark: (markId: string) => Promise; + /** 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; + queryMark: QueryMark; + markType: MarkType; +}; +export const createManagerStore: StateCreator = (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; diff --git a/src/pages/App.tsx b/src/pages/App.tsx index da669ee..db7a6bb 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -1,5 +1,10 @@ import { basename } from '../modules/basename'; console.log('basename', basename); +import { App as MarkApp } from '../manager/Manager'; export const App = () => { - return
; + return ( +
+ +
+ ); }; diff --git a/vite.config.ts b/vite.config.ts index c8db331..76eaa25 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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, }, }, });