React v18+React-router-dom v6+Vite4后台管理系统搭建

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

2.选择react和javascript


3.安装Antd Design组件和相应的Icon

js 复制代码
npm install antd --save
npm install @ant-design/icons --save
npm install react-router-dom --save
  1. 目录结构
  1. router的使用:
  • 路由懒加载:如果不使用懒加载技术,则在第一次加载项目的时候会进行加载所有组件资源,当组件过多的时候会出现页面卡死的状态,当使用懒加载的时候,只有当路由进行跳转的时候才进行下载该组件,提高渲染性能
  • react路由懒加载的原理是通过ES6的import函数动态加载特性(import()函数介绍)和React.lazy()、Supense实现的
  • 使用方式:
jsx 复制代码
// 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>
)
jsx 复制代码
// ./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绑定的函数)

正确的写法:

jsx 复制代码
    <ArrowsAltOutlined  className="resizeIcon" onClick={()=> jumpToDetail(item.title)}/>
jsx 复制代码
    import {useNavigate} from 'react-router-dom'
    const navigateTo = useNavigate();
    const jumpToDetail = (id)=> {
       navigateTo('/WallpaperDetail', {state: {id}});
    }
  1. useLocation参数的接收
jsx 复制代码
import useLocation from 'react-router-dom'
const location = useLocation();
const {state} = location;
  1. 父子组件之间的传参
    • 父组件通过props向子组件进行传值
    • 父组件通过props向子组件传递一个函数,然后子组件通过props获取函数并传递参数,父组件通过函数拿到子组件传递来的值
    • 下面是封装一个手动上传文件的upload组件(beforeUpload 返回 false 后,通过getFileName通知父组件上传的文件,)
jsx 复制代码
// 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;
jsx 复制代码
//父组件
 <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状态的按钮组件
jsx 复制代码
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弹框,"上传/替换预览图"
  • 效果如下:
jsx 复制代码
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-9\s]+$/.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" width="30" height="30" 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函数的复杂度)
jsx 复制代码
const title = useMemo(()=> {
    if (oprateType === 'import') {
      return '自动导入资源'
    } else {
      return '多选编辑'
    }
  }, [oprateType])
相关推荐
布瑞泽的童话21 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
白鹭凡33 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_8628867837 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
书中自有妍如玉44 分钟前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn1 小时前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f8979070701 小时前
layui 可以使点击图片放大
前端·javascript·layui
忘不了情1 小时前
左键选择v-html绑定的文本内容,松开鼠标后出现复制弹窗
前端·javascript·html
世界尽头与你1 小时前
HTML常见语法设计
前端·html
写bug如流水1 小时前
【Git】Git Commit Angular规范详解
前端·git·angular.js