React18+React-router-dom6+Vite4后台办理体系建立

背景
经过AI东西(Stable Diffusion)为用户供给更多的壁纸选项资源,以满足用户对壁纸多样性和个性化的需求,并经过预设的模型生成更贴合公司风格的壁纸。
该体系主要是对这些AI壁纸资源进行办理、提示词库办理、壁纸资源拜访和下载规矩、描述语句格局配置等。

技术选型
React V18
Vite V4
react-router-dom V6
Javascript
Antd Design

项目建立进程

  1. npm create vite AIWallpaper

React v18+React-router-dom v6+Vite4后台办理体系建立

2.选择react和javascript

React v18+React-router-dom v6+Vite4后台办理体系建立

3.装置Antd Design组件和相应的Icon
npm install antd --save
npm install @ant-design/icons --save
npm install react-router-dom --save
  1. 目录结构

React v18+React-router-dom v6+Vite4后台办理体系建立

5. router的运用:

  • 路由懒加载:假如不运用懒加载技术,则在第一次加载项目的时分会进行加载所有组件资源,当组件过多的时分会呈现页面卡死的状态,当运用懒加载的时分,只有当路由进行跳转的时分才进行下载该组件,提高烘托性能
  • react路由懒加载的原理是经过ES6的import函数动态加载特性(import()函数介绍)和React.lazy()、Supense完成的
  • 运用方式:
// main.jsx
import React, { Suspense } from 'react'
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom/client'
import './index.css'
import {
  createHashRouter,
  RouterProvider,
  Navigate
} from "react-router-dom";
import LayoutComp from './layout';
import store  from './store/index';
import { WallpaperDetail,  PromptsManager, SentencesManager, WallpaperManager, Login} from './component.jsx'
function VerifyLogin(prop) {
  const userInfo = JSON.parse(localStorage.getItem('userInfo'));
  return userInfo ? prop.element: <Navigate to="/login" replace />;
}
const router = createHashRouter([
  {
    path: "/",
    element: <Navigate to="/login" />
  },
  {
    path: "/",
    element: <LayoutComp />,
    children: [
      {
        path: '/PromptsManager',
        element: <VerifyLogin element={<PromptsManager />} />,
      },
      {
        path: '/wallpaperManager',
        element: <VerifyLogin element={<WallpaperManager />} />,
      },
      {
        path: '/sentencesManager',
        element: <VerifyLogin element={<SentencesManager />} />,
      },
      {
        path: '/WallpaperDetail',
        element: <VerifyLogin element={<WallpaperDetail />} />,
      }
    ]
  },
  {
    path: '/login',
    element: <Login/>
  }
]);
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <Suspense fallback="..loading">
        <RouterProvider router={router} />
      </Suspense>
    </Provider>
  </React.StrictMode>
)
// ./component.jsx文件
import {lazy} from 'react'
export const WallpaperDetail = lazy(() => import('./views/wallpaperDetail'))
export const SentencesManager = lazy(() => import('./views/SentencesManager'))
export const PromptsManager = lazy(() => import('./views/PromptsManager'))
export const WallpaperManager = lazy(() => import('./views/WallpaperManager'))
export const Login = lazy(() => import('./views/login'))
  1. useNavigate用来跳转和传值
    注意:事件传参需求用到箭头函数,不然会报下列过错(组件烘托的时分会立即执行onClick绑定的函数)

React v18+React-router-dom v6+Vite4后台办理体系建立

正确的写法:

    <ArrowsAltOutlined  className="resizeIcon" onClick={()=> jumpToDetail(item.title)}/>
    import {useNavigate} from 'react-router-dom'
    const navigateTo = useNavigate();
    const jumpToDetail = (id)=> {
       navigateTo('/WallpaperDetail', {state: {id}});
    }
  1. useLocation参数的接收
import useLocation from 'react-router-dom'
const location = useLocation();
const {state} = location;
  1. 父子组件之间的传参
    • 父组件经过props向子组件进行传值
    • 父组件经过props向子组件传递一个函数,然后子组件经过props获取函数并传递参数,父组件经过函数拿到子组件传递来的值
    • 下面是封装一个手动上传文件的upload组件(beforeUpload返回false后,经过getFileName通知父组件上传的文件,)
// uploadButtons子组件
import {  Upload, Space, message } from 'antd';
import {  PlusOutlined } from '@ant-design/icons';
import { useState } from 'react'
import PropTypes from 'prop-types'
import { useEffect } from 'react';
const UploadRender = ({
  type,
  getFileName
}) => {
  const [fileName, setFileName] = useState('');
  const beforeUpload = (file) => {
    if (type === 'languagePack') {
      juegeLanPackType(file)
    } else if (type === 'img'){
      judgeImgType(file)
    }
  }
  useEffect(()=>{
    setFileName('')
  }, [])
  // 判断上传图片类型
  const judgeImgType = (file) => {
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    if (!isJpgOrPng) {
      message.error('文件类型类型可所以PNG、JPG!');
    }
    const isLt5M = file.size / 1024 / 1024 < 5 || file.size / 1024 / 1024 === 5;
    if (!isLt5M) {
      message.error('文件巨细不超越5M!');
    }
    setFileName(file.name)
    getFileName(file)
    return false
  }
  // 判断上传语言包类型
  const juegeLanPackType = (file) => {
    const isZAROrZIP = file.type === 'application/x-zip-compressed' || file.type === 'image/png';
    if (!isZAROrZIP) {
      message.error('文件类型类型可所以RAR、ZIP格局!');
    }
    // const isLt10M = file.size / 1024 / 1024 < 10 || file.size / 1024 / 1024 === 10;
    // if (!isLt10M) {
    //   message.error('文件巨细不超越10M!');
    // }
    return isZAROrZIP;
  }
  const uploadButton = function() {
    let item = 
    <Space>
      <PlusOutlined />
      {type === 'img' ?  <span>预览图</span> : <span>语言包</span> }
    </Space>
    if (fileName) {
      item = <span>{fileName}</span>
    }
    return <div className="uploadBtn">{item}</div>
  }
  return <>
      <p style={{fontWeight: '600'}}>{type === 'img' ? '预览图' : '语言包'}</p>
      <p>{ type === 'img' ? '文件巨细不超越5M,文件类型可所以PNG、JPG格局' : '上传json格局的翻译资源,文件类型可所以RAR、ZIP格局'}</p>
      <div style={{display: 'flex', justifyContent: 'center', marginTop: '10px'}}>
        <Upload
          name="avatar"
          showUploadList={false}
          action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
          beforeUpload={beforeUpload}
        >
          { uploadButton() }
        </Upload>
      </div>
  </>
}
UploadRender.propTypes = {
  type: PropTypes.any,
  getFileName: PropTypes.func,
}
export default UploadRender;
//父组件
 <UploadRender
      type='img'
      rawFileName=''
      getFileName={getChildFileName}/>
  const getChildFileName = (rawFile) => {
    setFileList([...fileList, rawFile])
  }
  1. react中的类型检查
    • 装置特点查验包:npm install prop-types –save
    • ProTypes供给了不同的验证器 –官方文档
      这儿封装了一个经过antd-design中的validateFields的vlidateOnly动态调整按钮的disabled状态的按钮组件
import React from 'react'
import { Form, Button } from 'antd'
import PropTypes from 'prop-types';
const EditButton = ({children, form}) => {
  const [isDisable, setDisable] = React.useState(false)
  const values = Form.useWatch([], form)
  React.useEffect(() => {
    form
      .validateFields({
        validateOnly: true,
      })
      .then(
        () => {
          setDisable(true);
        },
        () => {
          setDisable(false);
        },
      );
  }, [values]);
  return (
    <Button type='primary' disabled={!isDisable}>{children}</Button>
  )
}
EditButton.propTypes = {
  form: PropTypes.object,
  children: PropTypes.node
}
export default EditButton
  1. antd design中的table表格自定义
  • 当需求自定义的单元格较多,且逻辑较复杂的时分封装单元格组件
  • table组件供给了components特点用于接受单元格的组件
  • 如下:表格中的预览图有返回的Url则烘托img标签,不然烘托button,鼠标悬浮img呈现遮罩层,显现可替换的按钮,点击可替换按钮和上传预览图的button,呈现Modal弹框,“上传/替换预览图”
  • 效果如下:

React v18+React-router-dom v6+Vite4后台办理体系建立

import React, { useState, useContext, useRef, useEffect} from "react"
import { message, Modal,  Button, Form, Input} from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import PropTypes from 'prop-types'
import './editCell.css'
import UploadRender from './uploadButtons'
import {getUploadUrl, uploadFile, notifyUploadSucess, modifyDescName} from '../Api/api'
export const EditCell = ({
  children,
  dataIndex,
  preview_image_url,
  record,
  getDescriptionList,
  ...restProps
}) => {
  const form = useContext(EditableContext);
  const [openModal, setOpenModal] = useState(false);
  const [loading, setLoading] = useState(false);
  const [fileList, setFileList] = useState([]);
  const [currentId, setCurrentId] = useState(''); //当时点击的描述词
  const [editing, setEditing] = useState(false);
  const inputRef = useRef(null);
  const replacePreview = (id) => {
    setOpenModal(true)
    setCurrentId(id)
  }
  useEffect(() => {
    if (editing) {
      inputRef.current.focus();
    }
  }, [editing]);
  // 预览图模态框确认
  const handleOk = async () => {
    try {
      let { name, type } = fileList[0]
      setLoading(true)
      let {code, data } = await getUploadUrl({resource_type: 1, file_name: name});
      if (code === '00000') {
        let { status } = await uploadFile(data.s3_upload_url, fileList[0], type);
        if (status === 200) {
          try {
            let result = await notifyUploadSucess({s3_file_name: data.file_name, description_word_id: currentId});
            if (result.code === '00000') {
              message.success('上传成功!');
              setOpenModal(false)
              getDescriptionList();
            }
          }catch(err) {
            message.error('上传失利!');
          }
        }
      }
    }catch(err) {
      message.error('上传失利!');
    } finally {
      setLoading(false)
    }
  }
  const toggleEdit = () => {
    if (record.status !== 0)
    return;
    setEditing(!editing);
    form.setFieldsValue({
      [dataIndex]: record[dataIndex],
    });
  };
  // 预览图模态框撤销
  const handleCancel = () => {
    setOpenModal(false)
  }
  const getChildFileName = (rawFile) => {
    setFileList([...fileList, rawFile])
  }
  const save = async () => {
    try {
      const values = await form.validateFields();
      console.log(values, 'values1111')
      toggleEdit();
      if (record.name === values.name) return;
      if (!(/^[u4e00-u9fa5_a-zA-Z0-9s]+$/.test(values.name))) {
        message.error('描述词不能包括特殊字符')
        return
      }
      handleSave(values.name)
    } catch (errInfo) {
      console.log('Save failed:', errInfo);
    }
  }
  const handleSave = (name) => { 
    modifyDescName({description_word_id: record.id, new_name: name}).then(res => {
      let { code } = res;
      if (code === '00000') {
        message.success('描述词更新成功!')
        getDescriptionList();
      } else {
        message.error(res.msg)
      }
    })
  };
  let childNode = children;
  if (dataIndex === 'preview_image_url' ) {
    if (preview_image_url) {
      // setImageUrl(url)
      <img src={preview_image_url}></img>
      childNode = (
        <div className="imgWrap">
          <div className="imgMask"></div>
          <img
            src={preview_image_url}
            alt="avatar"
            style={{
              width: '100%',
              height: '100%'
            }}
          />
          <svg onClick={ () => replacePreview(record.id)} className="iconRotate" title="替换预览图" xmlns="http://www.w3.org/2000/svg"   viewBox="0 0 40 40" fill="none">
            <path d="M25.1953 39.229C24.0724 39.2134 23 38.7604 22.2059 37.9664C21.4119 37.1723 20.9589 36.0998 20.9432 34.977V24.752C20.9594 23.6293 21.4125 22.5571 22.2065 21.7631C23.0004 20.9692 24.0726 20.516 25.1953 20.4999H35.4203C36.5435 20.5144 37.6166 20.9671 38.4109 21.7614C39.2052 22.5557 39.6578 23.6288 39.6724 24.752V34.977C39.6583 36.1003 39.2059 37.1737 38.4115 37.9681C37.6171 38.7625 36.5437 39.215 35.4203 39.229H25.1953ZM24.0766 24.752V34.977C24.0933 35.2686 24.2165 35.544 24.4227 35.7509C24.6288 35.9578 24.9037 36.082 25.1953 36.0999H35.4203C35.7132 36.0823 35.9896 35.9586 36.1978 35.7519C36.4061 35.5452 36.5318 35.2697 36.5516 34.977V24.752C36.5313 24.4598 36.4052 24.1851 36.197 23.9791C35.9888 23.7732 35.7127 23.6502 35.4203 23.6332H25.1953C24.9033 23.6495 24.6274 23.7724 24.4201 23.9786C24.2127 24.1849 24.0882 24.46 24.0703 24.752H24.0766ZM5.98073 35.3645C3.8903 33.1055 2.69166 30.164 2.60781 27.0874H0.984896L4.21198 21.704L7.45156 27.0874H5.73073C5.81158 29.3596 6.69768 31.5291 8.23073 33.2082C10.1872 35.0821 12.7967 36.1201 15.5057 36.102C15.7177 36.091 15.9297 36.1233 16.1288 36.1969C16.3279 36.2705 16.5099 36.3838 16.6639 36.5299C16.8178 36.6761 16.9403 36.8521 17.0241 37.0471C17.1078 37.2422 17.151 37.4522 17.151 37.6645C17.151 37.8767 17.1078 38.0868 17.0241 38.2818C16.9403 38.4768 16.8178 38.6528 16.6639 38.799C16.5099 38.9451 16.3279 39.0585 16.1288 39.132C15.9297 39.2056 15.7177 39.2379 15.5057 39.227C11.9457 39.2401 8.52409 37.8492 5.98281 35.3561L5.98073 35.3645ZM4.5724 19.504C3.44987 19.4874 2.37799 19.034 1.58416 18.2402C0.790324 17.4464 0.336985 16.3745 0.320312 15.252V5.04362C0.334285 3.91908 0.786369 2.84435 1.58043 2.04795C2.3745 1.25155 3.4479 0.796313 4.5724 0.779039H14.7974C15.9221 0.795787 16.9958 1.25086 17.7899 2.04737C18.5841 2.84388 19.036 3.91892 19.0495 5.04362V15.252C19.0556 15.8121 18.9498 16.3677 18.7383 16.8864C18.5268 17.4051 18.2138 17.8762 17.8177 18.2723C17.4217 18.6684 16.9505 18.9814 16.4318 19.1929C15.9132 19.4044 15.3575 19.5102 14.7974 19.504H4.5724ZM3.44115 5.03529V15.2436C3.46112 15.5371 3.58671 15.8133 3.79469 16.0213C4.00267 16.2293 4.27895 16.3549 4.5724 16.3749H14.7974C14.9476 16.381 15.0975 16.356 15.2375 16.3013C15.3775 16.2467 15.5047 16.1636 15.611 16.0573C15.7173 15.951 15.8005 15.8238 15.8551 15.6837C15.9098 15.5437 15.9348 15.3938 15.9286 15.2436V5.03529C15.9109 4.74108 15.786 4.46356 15.5775 4.25514C15.3691 4.04672 15.0916 3.92183 14.7974 3.90404H4.5724C4.27926 3.92275 4.00303 4.04779 3.79552 4.25568C3.58801 4.46357 3.46349 4.74004 3.44531 5.03321L3.44115 5.03529ZM32.5516 13.0957H34.2766C34.2345 10.7646 33.346 8.52829 31.7766 6.80404C29.8173 4.92306 27.2008 3.88172 24.4849 3.90196C24.0902 3.87311 23.7211 3.69597 23.4516 3.40612C23.1822 3.11627 23.0324 2.7352 23.0324 2.33946C23.0324 1.94372 23.1822 1.56264 23.4516 1.27279C23.7211 0.982941 24.0902 0.805799 24.4849 0.776956C28.0464 0.763234 31.47 2.15317 34.0141 4.64571C36.148 6.95042 37.3564 9.96148 37.4078 13.102H39.012L35.9641 18.4853L32.5516 13.0957Z" fill="white"/>
          </svg>
          <Modal
            title="替换预览图"
            footer={[
              <Button key="submit" type="primary" loading={loading} onClick={handleOk}>
                完成
              </Button>,
              <Button key="back" onClick={handleCancel}>
                撤销
              </Button>,
            ]}
            open={openModal}
            >
            <UploadRender
              type='img'
              rawFileName=''
              getFileName={getChildFileName}/>
          </Modal>
        </div>
      )
    } else {
      childNode = (
        <>
          <div className='uploadTip' onClick={() => replacePreview(record.id)}>
            <PlusOutlined style={{fontSize: '20px', color: '#eee'}}/>
          </div>
          <span>上传预览图</span>
          <Modal
            title="上传预览图"
            footer={[
              <Button key="submit" type="primary" loading={loading} onClick={handleOk}>
                完成
              </Button>,
              <Button key="back" onClick={handleCancel}>
                撤销
              </Button>,
            ]}
            open={openModal}>
            <UploadRender
              type='img'
              rawFileName=''
              getFileName={getChildFileName}/>
          </Modal>
        </>
      )
    }
    return <td {...restProps}>{ childNode }</td>
  }
  if (dataIndex === 'name') {
    childNode = editing ? (
      <Form.Item
        style={{
          margin: 0,
        }}
        name={dataIndex}
        rules={[
          {
            required: true,
            message: ` is required.`,
          },
        ]}
      >
        <Input ref={inputRef} onPressEnter={save} onBlur={save} />
      </Form.Item>
    ) : (
      <div
        className={record.status === 0 ? 'editable-cell-value-wrap' : ''}
        style={{
          paddingRight: 24,
        }}
        onClick={toggleEdit}
      >
        {children} 
      </div>
    );
  }
  return <td {...restProps}>{ childNode }</td>
}
EditCell.propTypes  = {
  dataIndex: PropTypes.any,
  preview_image_url: PropTypes.any,
  children: PropTypes.node,
  record: PropTypes.object,
  getDescriptionList: PropTypes.func
}
const EditableContext = React.createContext(null);
export const EditableRow = ({ index, ...props }) => {
  const [form] = Form.useForm();
  return (
    <Form form={form} component={false}>
      <EditableContext.Provider value={form}>
        <tr {...props} />
      </EditableContext.Provider>
    </Form>
  );
};
  1. 运用React Hooks-> useMemo优化核算特点
    在组件的顶层调用useMemo来烘托每次重新烘托都需求重新核算的成果 vue中的核算特点一方面能够简写语法,另一方面可能够被缓存 (根据依赖缓存核算成果,完成逻辑核算与视图烘托的解耦,降低render函数的复杂度)
const title = useMemo(()=> {
    if (oprateType === 'import') {
      return '主动导入资源'
    } else {
      return '多选编辑'
    }
  }, [oprateType])