增加组件

This commit is contained in:
WenC 2024-03-12 18:53:51 +08:00
parent 2cd8e574d8
commit b969560ee6
19 changed files with 602 additions and 6 deletions

177
package-lock.json generated
View File

@ -8,9 +8,16 @@
"name": "ai-copyright",
"version": "0.0.0",
"dependencies": {
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"antd": "^5.15.2",
"localforage": "^1.10.0",
"match-sorter": "^6.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"sort-by": "^1.2.0",
"stylus": "^0.63.0"
},
"devDependencies": {
@ -953,6 +960,64 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
"hasInstallScript": true,
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
"integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -1203,6 +1268,14 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
"integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz",
@ -2630,6 +2703,11 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -2803,6 +2881,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -2844,6 +2938,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/match-sorter": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
"dependencies": {
"@babel/runtime": "^7.23.8",
"remove-accents": "0.5.0"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -2916,6 +3019,22 @@
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-path": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz",
"integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -3073,6 +3192,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -3719,11 +3853,46 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "6.22.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
"integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
"dependencies": {
"@remix-run/router": "1.15.3"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.22.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
"integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
"dependencies": {
"@remix-run/router": "1.15.3",
"react-router": "6.22.3"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@ -3902,6 +4071,14 @@
"node": ">=8"
}
},
"node_modules/sort-by": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz",
"integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==",
"dependencies": {
"object-path": "0.6.0"
}
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",

View File

@ -10,9 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"antd": "^5.15.2",
"localforage": "^1.10.0",
"match-sorter": "^6.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"sort-by": "^1.2.0",
"stylus": "^0.63.0"
},
"devDependencies": {

View File

@ -1,11 +1,16 @@
import React from 'react';
import Head from './layout/head/Head.tsx';
import Nav from "./layout/nav/Nav.tsx";
import Body from './layout/body/Body.tsx';
import Foot from './layout/foot/Foot.tsx';
const App: React.FC = () => {
return (
<div>
<Head/>
<Nav/>
<Body/>
<Foot/>
</div>
);

View File

@ -0,0 +1,78 @@
import './menu-tree.css';
import {useState} from "react";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faCaretRight, faCaretDown, faEdit, faPlus, faRemove} from '@fortawesome/free-solid-svg-icons';
import {IMenuTree, IMenuTreeItem} from "../../interfaces/menu/IMenuTree.ts";
export default function MenuTree(props: IMenuTree) {
const [menusArray, setMenuArray] = useState([
{
id: '1',
name: '一级目录',
active: true,
level: 1,
children: [
{
id: '1-1',
name: '二级目录',
active: false,
level: 1,
children: [
{
id: '1-1-1',
name: '三级目录',
active: false,
level: 1,
children: []
}
]
}
]
}
]);
const triggerChildren = (item: IMenuTreeItem) => {
item.active = !item.active;
setMenuArray([
...menusArray
]);
}
const renderMenu = (children: Array<IMenuTreeItem>, parent?: IMenuTreeItem) => {
if (!children || children.length == 0) {
return <></>
}
const lis = children.map((item, index) => {
const isParent = item.children && item.children.length > 0;
const icon = item.active ? faCaretDown : faCaretRight;
const renderChildrenMenu = renderMenu(item.children, item);
return (
<li key={item.id}>
<div className="menu-title">
<div className="label">
{isParent ? <FontAwesomeIcon className="icon" icon={icon} onClick={() => {
triggerChildren(item)
}}/> : <></>}
<span onDoubleClick={() => {
triggerChildren(item)
}}>{item.name}</span>
</div>
<div className="icon-group">
<FontAwesomeIcon className="icon" icon={faEdit}/>
<FontAwesomeIcon className="icon" icon={faPlus}/>
<FontAwesomeIcon className="icon" icon={faRemove}/>
</div>
</div>
{renderChildrenMenu}
</li>
)
})
const active = parent ? parent.active : true;
return <ul style={{display: active ? 'block' : 'none'}}>{lis}</ul>
}
const menuUl = renderMenu(menusArray);
return <div className="menu-tree">{menuUl}</div>
}

View File

@ -0,0 +1,26 @@
import './menu-with-top-button.css'
import {IMenuWithTopButton} from "../../interfaces/menu/IMenuWithTopButton.ts";
export default function MenuWithTopButton(props: IMenuWithTopButton) {
const list = props.list.map((item, index) => (
<li key={item.id} onClick={(e) => {
props.handleListItem(e, index, item);
}}>
<img src={item.icon} className="menu-icon" alt="加载失败"/>
<span className="menu-name">{item.name}</span>
</li>
));
return (
<div className="menu-with-top-button">
<button className="btn btn-blue top-button"
onClick={(e) => {
props.button.handle(e);
}}
>
{props.button.name}
</button>
<ul>{list}</ul>
</div>
)
}

View File

@ -0,0 +1,27 @@
.menu-tree {
padding: 0 15px;
}
.menu-tree ul {
margin-left: 10px;
}
.menu-tree ul li {}
.menu-tree ul li .menu-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-tree ul li .menu-title .label {
cursor: pointer;
}
.menu-tree ul li .menu-title .label span {
margin-left: 5px;
}
.menu-tree ul li .menu-title .icon-group {
width: 50px;
flex-shrink: 0;
display: flex;
justify-content: space-between;
}
.menu-tree ul li .menu-title .icon-group .icon {
cursor: pointer;
}

View File

@ -0,0 +1,36 @@
.menu-with-top-button {
padding: 15px;
}
.menu-with-top-button .top-button {
width: 100%;
}
.menu-with-top-button ul {
padding: 10px;
background-color: yellow;
}
.menu-with-top-button ul li {
display: flex;
align-items: center;
padding: 5px;
cursor: pointer;
}
.menu-with-top-button ul li:hover {
background-color: var(--color-hover);
}
.menu-with-top-button ul li:last-child {
margin-bottom: 0;
}
.menu-with-top-button ul li .menu-icon {
width: 30px;
height: 30px;
}
.menu-with-top-button ul li .menu-name {
padding-left: 5px;
}

View File

@ -8,7 +8,11 @@
--color-purple: #a233c6;
--color-dark: #2f363c;
--color-light: #fafafa;
--color-hover: #eeeeee;
--font-size-head: 16px;
--width-workspace: 1280px;
--height-head: 60px;
--height-foot: 30px;
}
html, body {
@ -16,3 +20,55 @@ html, body {
padding: 0;
font-size: 14px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.btn {
border: none;
border-radius: 3px;
padding: 10px;
cursor: pointer;
}
.btn:active {
opacity: 0.95;
}
.btn-red {
background-color: var(--color-red);
color: var(--color-light);
}
.btn-orange {
background-color: var(--color-orange);
color: var(--color-light);
}
.btn-green {
background-color: var(--color-green);
color: var(--color-light);
}
.btn-blue {
background-color: var(--color-blue);
color: var(--color-light);
}
.btn-purple {
background-color: var(--color-purple);
color: var(--color-light);
}
.btn-dark {
background-color: var(--color-dark);
color: var(--color-light);
}
.btn-light {
background-color: var(--color-light);
color: var(--color-dark);
}

View File

@ -0,0 +1,15 @@
import {MouseEvent} from 'react';
export interface IMenuTreeItem {
id: string;
name: string;
active: boolean;
level: number;
children: Array<IMenuTreeItem>;
}
export interface IMenuTree {
menus: Array<IMenuTreeItem>;
handleClick(e: MouseEvent<HTMLSpanElement>, item: IMenuTreeItem): void;
}

View File

@ -0,0 +1,22 @@
import {MouseEvent} from "react";
export interface IMenuListItem {
id: string;
icon: string;
name: string;
}
export interface IMenuButton {
name: string;
handle(e: MouseEvent<HTMLButtonElement>): void;
}
export interface IMenuWithTopButton {
button: IMenuButton;
list: Array<IMenuListItem>;
handleListItem(e: MouseEvent<HTMLLIElement>, index: number, item: IMenuListItem): void;
}

19
src/layout/body/Body.tsx Normal file
View File

@ -0,0 +1,19 @@
import './body.css'
import {createBrowserRouter, RouterProvider} from 'react-router-dom';
import Index from '../../route/index';
const router = createBrowserRouter([
{
path: '/',
element: <Index />
}
])
export default function Body() {
const winHeight: number = window.innerHeight
return (
<div className="body" style={{height: `${winHeight - 145}px`}}>
<RouterProvider router={router}/>
</div>
)
}

5
src/layout/body/body.css Normal file
View File

@ -0,0 +1,5 @@
.body {
margin: 0 auto 10px auto;
background-color: red;
width: var(--width-workspace);
}

View File

@ -2,6 +2,6 @@ import './foot.css';
export default function Foot() {
return (
<div className="foot">footer</div>
<div className="foot"></div>
)
}

View File

@ -3,7 +3,7 @@
bottom: 0;
left: 0;
width: 100%;
height: 30px;
height: var(--height-foot);
display: flex;
justify-content: center;
align-items: center;

View File

@ -1,6 +1,6 @@
.head {
width: 100%;
height: 60px;
height: var(--height-head);
position: fixed;
top: 0;
left: 0;
@ -12,7 +12,7 @@
.head .center {
margin: 0 auto;
max-width: 1280px;
width: var(--width-workspace);
height: 100%;
display: flex;
justify-content: space-between;

19
src/layout/nav/Nav.tsx Normal file
View File

@ -0,0 +1,19 @@
import './nav.css';
export default function Nav() {
return (
<div className="nav">
<ul>
<li>
<span></span>
</li>
<li>
<span></span>
</li>
<li>
<span></span>
</li>
</ul>
</div>
)
}

22
src/layout/nav/nav.css Normal file
View File

@ -0,0 +1,22 @@
.nav {
width: var(--width-workspace);
margin: calc(var(--height-head) + 15px) auto 10px auto;
}
.nav ul {
display: flex;
align-items: center;
}
.nav ul li {
}
.nav li::after {
content: ' > ';
padding: 0 5px;
}
.nav li:last-child::after {
content: ''
}

18
src/route/index/index.css Normal file
View File

@ -0,0 +1,18 @@
.index {
position: relative;
height: 100%;
}
.index .left {
position: absolute;
width: 220px;
height: 100%;
top: 0;
left: 0;
}
.index .right {
margin-left: 235px;
height: 100%;
background-color: green;
}

64
src/route/index/index.tsx Normal file
View File

@ -0,0 +1,64 @@
import './index.css';
import {MouseEvent} from "react";
import {IMenuListItem, IMenuWithTopButton} from "../../interfaces/menu/IMenuWithTopButton.ts";
import MenuWithTopButton from "../../components/menu/MenuWithTopButton.tsx";
import MenuTree from "../../components/menu/MenuTree.tsx";
const projMenu: IMenuWithTopButton = {
button: {
name: '创建项目',
handle(e: MouseEvent<HTMLButtonElement>) {
console.log(e)
}
},
list: [
{id: 'proj1', icon: './vite.svg', name: '全部项目'},
{id: 'proj2', icon: './vite.svg', name: '进行中的'},
{id: 'proj3', icon: './vite.svg', name: '已完成的'}
],
handleListItem(e: MouseEvent<HTMLLIElement>, index: number, item: IMenuListItem) {
console.log(e);
console.log(index);
console.log(item)
}
}
const agentMenu: IMenuWithTopButton = {
button: {
name: '代理服务',
handle(e: MouseEvent<HTMLButtonElement>) {
console.log(e)
}
},
list: [
{id: 'agent1', icon: './vite.svg', name: '全部项目'},
{id: 'agent2', icon: './vite.svg', name: '进行中的'},
{id: 'agent3', icon: './vite.svg', name: '已完成的'},
],
handleListItem(e: MouseEvent<HTMLLIElement>, index: number, item: IMenuListItem) {
console.log(e);
console.log(index);
console.log(item)
}
}
export default function Index() {
return (
<div className="index">
<div className="left">
<MenuWithTopButton
button={projMenu.button}
list={projMenu.list}
handleListItem={projMenu.handleListItem}
/>
<MenuTree/>
<MenuWithTopButton
button={agentMenu.button}
list={agentMenu.list}
handleListItem={agentMenu.handleListItem}
/>
</div>
<div className="right"></div>
</div>
)
}