diff --git a/src/_test.tsx b/src/_test.tsx index b22835b..a458bb3 100644 --- a/src/_test.tsx +++ b/src/_test.tsx @@ -1,103 +1,281 @@ +import React, { useCallback, useState } from "react"; +import ReactDom from "react-dom"; +import Modal from "./components/modal/modal"; +import Upload from "./components/upload/upload"; +import Tab from "./components/tab/tab"; +import Table from "./components/table/table"; +import Pop from "./components/pop"; +import LazyPullRequest from "./components/lazyPullRequest"; -import React, { useState } from 'react'; -import ReactDom from 'react-dom' -import Modal from './components/modal/modal'; -import Upload from './components/upload/upload'; -import Tab from './components/tab/tab'; - -import * as GOJI from 'goji_ui' - +import * as GOJI from "goji_ui"; +const requestImg = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + { + src: "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF", + alt: "123", + }, + ]); + }, 1000); + }); +}; +const data = [ + { + id: "1", + name: "李四", + height: "180cm", + weight: "75KG", + }, + { + id: "2", + name: "张三", + height: "140cm", + weight: "20KG", + }, + { + id: "3", + name: "王五", + height: "120cm", + weight: "80KG", + }, + { + id: "4", + name: "李2", + height: "180cm", + weight: "75KG", + }, + { + id: "5", + name: "张3", + height: "140cm", + weight: "20KG", + }, + { + id: "6", + name: "王4", + height: "120cm", + weight: "80KG", + }, + { + id: "7", + name: "李7", + height: "180cm", + weight: "75KG", + }, + { + id: "8", + name: "张8", + height: "140cm", + weight: "20KG", + }, + { + id: "9", + name: "王9", + height: "120cm", + weight: "80KG", + }, + { + id: "10", + name: "李10", + height: "180cm", + weight: "75KG", + }, + { + id: "11", + name: "张11", + height: "140cm", + weight: "20KG", + }, + { + id: "12", + name: "王12", + height: "120cm", + weight: "80KG", + }, +]; function App() { - const [visible, setVisible] = useState(false) - const [ev, setEv] = useState(false) - return
- {/* 这是扩展的内容
} - items={[ - { - title: "tab1", - key: "tab1", - children:
test
- }, - { - title: "tab2", - key: "tab2", - children:
tab2
- } - ]} - /> */} - - -

test

- - { - setEv(true) - }} - hiddenStyle={{ - height: '0px', - overflow: 'hidden' - }} - tabContentVisible={ev} - extSelector={'[aria-label="tab"]'} - extension={
{ setEv(!ev) }} className="ext">这是扩展的内容
} - items={[ - { - title: "tab1", - key: "tab1", - children:
tab1
- }, - { - title: "tab2", - key: "tab2", - children:
tab2
- } - ]} - /> - - { - setVisible(false) - }} - visible={visible} - > -
- how to set default value for typescript interface field - -
-
+ const col = [ + { + key: "1", + title: "名字", + name: "name", + filters: [ + { label: "张三", value: "2" }, + { label: "李四", value: "1" }, + ], + }, + { + key: "2", + title: "身高", + name: "height", + }, + { + key: "3", + title: "体重", + name: "weight", + sort: (a: any, b: any) => parseInt(a.weight) - parseInt(b.weight), + }, + { + key: "options", + title: "操作", + name: "options", + render(item: any) { + return ( + <> + {item.id !== "3" + ? [ + 姓名:{item.name}} + content={<>身高:{item.height}} + > + 点击查看气泡 + , +
111
, + ] + : [ + } + title={<>图片懒加载} + > + 图片懒加载 + , +
111
, + ]} + + ); + }, + }, + ]; + const [visible, setVisible] = useState(false); + const [ev, setEv] = useState(false); + return ( +
+ {/* 涵盖1,3,4,5题 弹窗和气泡类似,没做 vx17606727572 */} + +

test

+ + { + setEv(true); + }} + hiddenStyle={{ + height: "0px", + overflow: "hidden", + }} + tabContentVisible={ev} + extSelector={'[aria-label="tab"]'} + extension={ +
{ + setEv(!ev); + }} + className="ext" + > + 这是扩展的内容 +
+ } + items={[ + { + title: "tab1", + key: "tab1", + children:
tab1
, + }, + { + title: "tab2", + key: "tab2", + children:
tab2
, + }, + ]} + /> - { - for (var i = 0; i < f.length; i++) { - console.log(f[i].name) - } - return new Promise((r, j) => { - setTimeout(() => { - r(f) - }, 1000); - }) - }} + { + setVisible(false); + }} + visible={visible} + > +
+ how to set default value for typescript interface field + +
+
- valueFilter={({ response }) => { - return (response as Record).url - }} - onComplete={(res: any[]) => { - console.log(res) - }} - > - 请选择文件 -
- + { + for (var i = 0; i < f.length; i++) { + console.log(f[i].name); + } + return new Promise((r, j) => { + setTimeout(() => { + r(f); + }, 1000); + }); + }} + valueFilter={({ response }) => { + return (response as Record).url; + }} + onComplete={(res: any[]) => { + console.log(res); + }} + > + 请选择文件 + + + ); } -ReactDom.render(, document.getElementById("app")) \ No newline at end of file +ReactDom.render(, document.getElementById("app")); diff --git a/src/components/lazyPullRequest/index.less b/src/components/lazyPullRequest/index.less new file mode 100644 index 0000000..e69de29 diff --git a/src/components/lazyPullRequest/index.tsx b/src/components/lazyPullRequest/index.tsx new file mode 100644 index 0000000..ea4174a --- /dev/null +++ b/src/components/lazyPullRequest/index.tsx @@ -0,0 +1,70 @@ +import React, { ReactNode, useEffect, useRef, useState } from "react"; +import styles from "./index.less"; + +export default ({ request }: { request: () => any }) => { + const [data, setData] = useState([]); + const ref = useRef(null); + const visibleRef = useRef(null); + useEffect(() => { + if (!visibleRef.current || !ref.current) return; + const obs = new IntersectionObserver( + (e) => { + e.forEach((element) => { + if (element.isIntersecting) { + request && + request().then((res: any) => { + console.log(res, "res"); + setData((v) => [...v, ...res]); + }); + } + }); + }, + { root: ref.current } + ); + obs.observe(visibleRef.current); + }, []); + useEffect(() => { + const imageList = Array.from(ref.current?.children); + + if (!ref.current) { + return; + } + const inter = new IntersectionObserver( + (entries) => { + entries.forEach((item: any) => { + if (item.isIntersecting) { + item.target.src = JSON.parse( + item.target.dataset?.value || "{}" + )?.src; + item.target.alt = JSON.parse( + item.target.dataset?.value || "{}" + )?.alt; + inter.unobserve(item.target) + } + }); + }, + { root: ref.current } + ); + imageList.map((el: any) => { + inter.observe(el); + }); + + }, [data]); + return ( +
+ {data?.map((el, i) => { + return ( + + ); + })} +
+
+ ); +}; diff --git a/src/components/list_select/list_select.tsx b/src/components/list_select/list_select.tsx index 6f4289d..6de2707 100644 --- a/src/components/list_select/list_select.tsx +++ b/src/components/list_select/list_select.tsx @@ -1,22 +1,62 @@ - -import React, { useState } from 'react'; -import uuid from 'uuid' +import React, { useState } from "react"; interface ListSelectProps { - items: Array - mode: "multiple" | 'single' - value: unknown | [unknown]; - onChange(value: unknown): void + items: Array<{ label: string; value: any }>; + mode: "multiple" | "single"; + value?: unknown | [unknown]; + onChange(value: unknown): void; } -export default function ListSelect({ - mode, - items -}: ListSelectProps) { - const [] = useState(null) - return
    - {items.map((i) => { - return
  • - })} -
-} \ No newline at end of file +export default function ListSelect({ mode, items, onChange }: ListSelectProps) { + const [currentValue, setCurrentValue] = useState(null); + const [changeValue, setChangeValue] = useState([]); + const onchangeValueSingle = (e: any, value: any) => { + setCurrentValue(e?.target?.checked && value); + onChange(value); + }; + const onchangeValueMutiple = (e: any, value: any) => { + if (e.target?.checked) { + setChangeValue((v) => { + onChange([...v, value]); + return [...v, value]; + }); + } else { + setChangeValue((v) => { + onChange(v.filter((vl) => vl !== value)); + return v.filter((vl) => vl !== value); + }); + } + }; + + return ( +
    + {mode === "single" && + items.map((i) => { + return ( +
  • + onchangeValueSingle(e, i.value)} + checked={currentValue === i.value} + type={"checkbox"} + key={i.value} + > + {i.label} +
  • + ); + })} + {mode === "multiple" && + items.map((i) => { + return ( +
  • + onchangeValueMutiple(e, i.value)} + type={"checkbox"} + key={i.value} + > + {i.label} +
  • + ); + })} +
+ ); +} diff --git a/src/components/pagination/index.less b/src/components/pagination/index.less new file mode 100644 index 0000000..05ec437 --- /dev/null +++ b/src/components/pagination/index.less @@ -0,0 +1,43 @@ +.pagination { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + li{ + display: inline-block; + min-width: 32px; + height: 32px; + margin-inline-end: 8px; + font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji'; + line-height: 30px; + text-align: center; + vertical-align: middle; + list-style: none; + background-color: transparent; + border: 1px solid transparent; + border-radius: 6px; + outline: 0; + cursor: pointer; + user-select: none; + &.disable{ + color:rgba(0, 0, 0, 0.5); + pointer-events: none; + } + &:hover{ + background-color: rgba(0, 0, 0, 0.06); + } + &.active{ + border-color: #1677ff; + color: #1677ff; + &:hover{ + background-color: transparent; + color: #69b1ff; + + } + } + } +} +:global{ + li{ + list-style: none; + } +} \ No newline at end of file diff --git a/src/components/pagination/index.tsx b/src/components/pagination/index.tsx new file mode 100644 index 0000000..222d737 --- /dev/null +++ b/src/components/pagination/index.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from "react"; +import styles from "./index.less"; +export default ({ + pageSize, + onChange, + total, +}: { + pageSize: number; + onChange: (pageSize: number, pageNo: number) => void; + total: number; +}) => { + const [activeIndex, setActiveIndex] = useState(0); + useEffect(() => { + onChange && onChange(pageSize, activeIndex + 1); + }, []); + const handleChange = (i: number) => { + setActiveIndex(i); + onChange(pageSize, i + 1); + }; + return ( + +
  • {"<"}
  • + {new Array(total).fill("").map((el, i) => { + return ( +
  • handleChange(i)} + key={i} + > + {i + 1} +
  • + ); + })} +
  • + {">"} +
  • +
    + ); +}; diff --git a/src/components/pop/index.less b/src/components/pop/index.less new file mode 100644 index 0000000..25850c4 --- /dev/null +++ b/src/components/pop/index.less @@ -0,0 +1,31 @@ +.popContanier { + background-color: #fff; + padding:12px; + box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + position: absolute; + z-index: 999; + border-radius: 10px; + top: 100%; + margin-top: 10px; + &::before { + content: ''; + width: 6px; + height: 6px; + background-color: white; + top:0 ; + left: 7px; + transform: rotate(45deg); + margin-top: -2px; + position: absolute; + } + .title{ + min-width: 177px; + margin-bottom: 8px; + color: rgba(0, 0, 0, 0.88); + font-weight: 600; + text-align: left; + } + .content{ + text-align: left; + } +} \ No newline at end of file diff --git a/src/components/pop/index.tsx b/src/components/pop/index.tsx new file mode 100644 index 0000000..9f78522 --- /dev/null +++ b/src/components/pop/index.tsx @@ -0,0 +1,37 @@ +import React, { useState, useRef, ReactNode } from "react"; +import styles from "./index.less"; +import { useClickAway } from "../../util/clickAway"; + +export default ({ + children, + title, + content, +}: { + children: string | ReactNode; + title:string | ReactNode; + content:string | ReactNode; +}) => { + const [visible, setVisible] = useState(false); + const ref = useRef(null); + const ContainerRef = useRef(null); + useClickAway(() => { + setVisible(false); + }, [ref, ContainerRef]); + return ( +
    +
    setVisible(!visible)} ref={ref}> + {children} +
    +
    + {visible && <> + {title &&
    + {title} +
    } + {content &&
    + {content} +
    } + } +
    +
    + ); +}; diff --git a/src/components/table/table.less b/src/components/table/table.less new file mode 100644 index 0000000..25ec95f --- /dev/null +++ b/src/components/table/table.less @@ -0,0 +1,54 @@ +.tableStyle{ + border-collapse: collapse; + width: 100%; + caret-color: transparent; + .lv{ + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + } + .littleFn { + color: blue; + cursor: pointer; + } + thead { + width: 100%; + + th { + justify-content: center; + flex: 1; + display: flex; + flex-wrap: nowrap; + padding: 16px; + position: relative; + background-color: #f2f2f2; + &:not(:last-child)::before { + content: ""; + position: absolute; + top: 50%; + inset-inline-end: 0; + width: 1px; + height:40px; + background-color: #ddd; + transform: translateY(-50%); + transition: background-color 0.2s; + } + } + + } + td{ + padding: 16px; + flex: 1; + text-align: center; + } + tr { + width: 100%; + display: flex; + flex-wrap: nowrap; + &:hover{ + background-color: #f2f2f2; + } + border-bottom: 1px solid #f0f0f0; + } +} \ No newline at end of file diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index f357ad6..c1a9334 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -1,52 +1,172 @@ - -import React, { ReactElement, useMemo } from 'react'; +import React, { ReactElement, ReactNode, useMemo, useState } from "react"; +import styles from "./table.less"; +import Pop from "../pop"; +import ListSelect from "../list_select/list_select"; +import Pagination from "../pagination"; interface IColProps { - key: string; - title: string; - render?(data: any): ReactElement + key: string; + title: string; + render?(data: any): ReactElement | ReactNode; + name: string; + sort?(a: any, b: any): number; + filters?: { label: string; value: string }[]; } interface ITableProps { - className?: string; - data: Array; - cols: Array + className?: string; + data: Array; + cols: Array; + [x: string]: any; } export default function Table(props: ITableProps) { - const { data, cols, className } = props; - - const col = useMemo(() => { - return cols?.map(c => { - return
    - }) - }, [cols]) - - return
    - {c?.title} -
    - - {col} - - - { - data?.map(d => { - return - {cols?.map(c => { - if (c.render) { - return - } - return - })} - - }) - } - -
    - { - c.render(d) - } - - {d[c.key]} -
    -} \ No newline at end of file + const { data, cols, className, ...remain } = props; + const [customData, setCustomData] = useState(data); + const { width, pagination } = remain || {}; + console.log(pagination); + /** + * @description 过滤函数 + * @param value{选中的值} + * @returns + */ + const filterFn = (value: any) => { + if (!value.length) { + setCustomData(data); + return; + } + let a = value.reduce((pre: any[], cur: any) => { + console.log( + pre, + customData.filter((el: any) => el.id === cur), + customData, + "asdasd" + ); + return [...pre, ...data.filter((el: any) => el.id === cur)]; + }, []); + setCustomData(a); + }; + /** + * @description 排序函数 + * @param mode {'desc' | 'asc'} + * @param cb (a:any,b:any)=>number + */ + const sortFn = (mode: "desc" | "asc", cb?: (a: any, b: any) => number) => { + if (mode === "asc") { + cb && + setCustomData((v) => { + const sortArr = v.sort(cb); + return [...sortArr]; + }); + } else { + cb && + setCustomData((v) => { + return [...v.sort(cb).reverse()]; + }); + } + }; + const col = useMemo(() => { + return cols?.map((c) => { + return ( + + {c?.title} + + {!!c?.filters?.length && ( + + } + > + + + )} + {!!c.sort && ( + <> + sortFn("asc", c.sort)} + > + 升 + + sortFn("desc", c.sort)} + > + 降 + + + )} + + {/* { +
    + 1: + 2: +
    + } */} + + ); + }); + }, [cols, customData]); + return ( + + + {col} + + + {customData?.map((d) => { + return ( + + {cols?.map((c) => { + if (c.render) { + if (c.key === "options") { + return ( + + ); + } + return ; + } + return ; + })} + + ); + })} + + {pagination && ( + + {/* 假设表格前端进行分页管理,数据一开始全请求到。 */} + { + setCustomData((v) => + data.slice( + (PageNo - 1) * pageSize, + (PageNo - 1) * pageSize + pageSize + ) + ); + }} + total={Math.ceil(data.length / pagination.pageSize)} + > + + )} +
    + {c.render(d)} + {c.render(d)}{d?.[c.name]}
    + ); +} diff --git a/src/util/clickAway.ts b/src/util/clickAway.ts new file mode 100644 index 0000000..f944387 --- /dev/null +++ b/src/util/clickAway.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +export const useClickAway = (cb:()=>void, refdom:any) => { + useEffect(()=>{ + const clickFn = (e:MouseEvent) => { + if(Array.isArray(refdom)){ + if(!!refdom.find((ref)=>ref.current?.contains(e.target as HTMLElement))){ + return; + } + cb && cb() + } else { + if(!refdom.current || refdom.current.contains(e.target as HTMLElement)) { + return; + } + cb && cb() + } + } + document.addEventListener('click',clickFn) + + return ()=>document.removeEventListener('click',clickFn) + },[]) +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 33d6dc6..6036f06 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,7 @@ const config = { path: path.resolve(__dirname, "./test") }, resolve: { - extensions: ['.js', '.tsx', '.jsx'], + extensions: ['.js', '.tsx', '.jsx', '.ts'], fallback: { 'react/jsx-runtime': 'react/jsx-runtime.js', 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', @@ -71,7 +71,7 @@ const config = { }, { - test: /\.(js|tsx)$/, + test: /\.(js|tsx|ts)$/, exclude: /node_modules/, use: { loader: "babel-loader", @@ -93,6 +93,9 @@ const config = { { loader: "style-loader" }, { loader: "css-loader", + options: { + modules: true, + } }, { loader: "less-loader",