利用现有Umi搭建的后台管理系统和所遇到的问题

一、用到的技术

1、技术栈

react: ^16.14.0

react-dom:^17.0.0

react-quill: ^1.3.5 富文本编辑器

react-router: ^4.3.1

react-dev-inspector:^1.1.1 这个包允许用户通过简单的点击直接从浏览器 React 组件跳转到本地 IDE 代码

2、UI组件

Ant Design --------------------------用到版本: antd: 4.15.1

Ant Design Pro Components

二、用法总结

1、富文本

这里有个坑,如果和自定义下拉选项一起使用,得下拉框渲染完之后才能渲染富文本组件,不然会报错,加不了必填项

import  ReactQuill,{ Quill }  from 'react-quill';
import QuillEmoji from 'quill-emoji'
import 'quill-emoji/dist/quill-emoji.css'
import 'react-quill/dist/quill.snow.css'

Quill.register({
  'modules/emoji-toolbar': QuillEmoji.ToolbarEmoji,
  // 'modules/emoji-textarea': QuillEmoji.TextAreaEmoji,
  'modules/emoji-shortname': QuillEmoji.ShortNameEmoji
})

  const modules={
    toolbar:{
      container:[
        [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
        [{ 'font': [] }],
        [{ 'header': 1 }, { 'header': 2 }],        // custom button values
        ['bold', 'italic', 'underline', 'strike'],    // toggled buttons
        [{ 'align': [] }],
        [{ 'indent': '-1' }, { 'indent': '+1' }],     // outdent/indent
        [{ 'direction': 'rtl' }],             // text direction
        [{ 'script': 'sub' }, { 'script': 'super' }],   // superscript/subscript
        ['blockquote', 'code-block'],
      
        [{ 'list': 'ordered' }, { 'list': 'bullet' }],
        [{ 'color': [] }, { 'background': [] }],
        ['emoji', 'image', 'video', 'link'],
      
        ['clean']
      ],
      handlers: {
        image: ()=>imageHandler()
      }
    },
    'emoji-toolbar': true,
    // 'emoji-textarea': true,
    'emoji-shortname': true,
  }
 //自定义上传图片格式
 const imageHandler = () => {
    const quillEditor = ref.current?.getEditor()
    const input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')
    input.click()
    input.onchange = async () => {
      const file = input.files[0]
      const formData = new FormData()
      formData.append('quill-image', file)
      const code=218
      const link=await upload(file,code)//把base64格式转换为正常url路径格式,自定义方法
      const range = quillEditor?.getSelection()
      quillEditor.insertEmbed(range.index, 'image', link)
    }
  }

{
    onselect.length>0&&//如果和自定义下拉选项一起使用,得下拉框渲染完之后才能渲染富文本组件,不然会报错
    <div  className={styles.box}>
            <Form.Item
              label="文章详情"
              name="articleContent"
              readonly={detailData?.id&&detailData?.edtil}
              // rules={[{ required: true, message: '请设置文章详情!' }]} 
            >
              <ReactQuill  modules={modules} ref={ref}/>
            </Form.Item>
            <div className={styles.mark}>*</div>
     </div>
 }
 

1.1、base64转url路径upload方法

import OSS from 'ali-oss';
import request from '@/utils/request';

const getConfig = (params = {}, options = {}) => {
  return request('/auth/goods/product/getOssConfig', {
    method: 'POST',
    data: params,
    ...options
  });
}

const getImageSize = (file) => {
  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = (theFile) => {
      const image = new Image()
      image.src = theFile.target.result
      image.onload = function () {
        const { width, height } = this;
        resolve({ width, height })
      }
    }
  });
}

let ossConfig = null;
let codeTemp = null

const upload = async (file, code) => {
  if (code !== codeTemp) {
    ossConfig = null
  }
  codeTemp = code;
  if (!ossConfig) {
    const res = await getConfig({ code });
    ossConfig = res?.data?.records
  }
  const client = new OSS({
    region: `oss-${ossConfig.regionId}`,
    accessKeyId: ossConfig.credentials.accessKeyId,
    accessKeySecret: ossConfig.credentials.accessKeySecret,
    stsToken: ossConfig.credentials.securityToken,
    bucket: ossConfig.uploadInfo.bucket,
  })
  return new Promise((resolve) => {
    client.put(`${ossConfig.uploadInfo.dir}/${file.uid}-y_g-${file.name}`, file).then(res => {
      if (file.type.indexOf('image') !== -1) {
        getImageSize(file).then(size => {
          resolve(`${res.url}?imgHeight=${size.height}&imgWidth=${size.width}`)
        })
      } else {
        resolve(res.url);
      }
    }).catch(err => {
      ossConfig = null;
      // return upload(file, dirName)
      console.log('上传失败:', err)
    })
  })
}

export default upload;

2、上传图片右边添加图片描述不影响Form表单提交的方法

企业微信截图_16451702601132.png

 const FromWrap = ({ value, onChange, content, right }) => (
    <div style={{ display: 'flex' }}>
      <div>{content(value, onChange)}</div>
      <div style={{ flex: 1, marginLeft: 10, minWidth: 180 }}>{detailData?.id&&detailData?.edtil?null:right(value)}</div>
    </div>
  )

<Form.Item
          label="封面图片"
          name="coverPicture"
          rules={[{ required: true, message: '请上传图片!' }]}
          readonly={detailData?.id&&detailData?.edtil} 
        >
          <FromWrap
          content={(value, onChange) => <Upload multiple value={value} onChange={onChange}   maxCount={1} accept="image/*"  size={(1*1024)/2} />}
          right={(value) => {
            return (
              <dl>
                <dt>图片要求</dt>
                <dd>1.图片大小500kb以内</dd>
                <dd>2.建议尺寸为 720 x 200</dd>
                <dd>3.图片格式png/jpg/gif</dd>
              </dl>
            )
          }}
        />
        </Form.Item>

3、省市区多选级联选择

企业微信截图_16451712318615.png

原级联组件地址:https://rsuitejs.com/components/multi-cascader/

import React, { useState, useEffect, useMemo } from 'react';
import { Button, Form, Input, message, Select, Tag } from 'antd';
import MultiCascader from 'rsuite/lib/MultiCascader';
import 'rsuite/lib/MultiCascader/styles';
import './style.less'

const AddressMultiCascader = ({ value = '', onChange = () => { }, data, pId = 0, ...rest }) => {
  const [areaData, setAreaData] = useState([]);
  const [selectAreaKey, setSelectAreaKey] = useState(value);
   
  const renderMultiCascaderTag = (selectedItems) => {
    const titleArr = [];
    selectedItems.forEach(item => {
      const arr = [];
      let node = item.parent;
      arr.push(item.label)
      while (node) {
        if (node.pvalue !==-1) {
          arr.push(node.label)
        }
        node = node.parent;
      }
      titleArr.push({
        label: arr.reverse().join('-'),
        value: item.value
      })
    })

  const arrayToTree = (list, parId = 0) => {
    const len = list.length
    function loop(pid) {
      const res = [];
      for (let i = 0; i < len; i += 1) {
        const item = list[i]
        if (item&&item.pid === pid) {
          item.children = loop(item.id)
          res.push(item)
        }
      }
      return res.length ? res : null
    }
    return loop(parId)
   }
    return (
      <div style={{ display: 'flex', flexWrap: 'wrap' }}>
        {
          titleArr.map(item => (
            <Tag
              key={item.value}
              closable
              style={{ marginBottom: 10 }}
              onClose={() => {
                setSelectAreaKey(selectAreaKey.filter(it => it !== item.value))
                onChange(selectAreaKey.filter(it => it !== item.value))
              }}
            >
              {item.label}
            </Tag>
          ))
        }
      </div>
    );
  }

  useEffect(() => {
    const arr = arrayToTree(data || window.yeahgo_area || [], pId)
    let str = JSON.stringify(arr)
    str = str.replace(/name/g, 'label').replace(/id/g, 'value')
    setAreaData(JSON.parse(str))
  }, [data])

  useEffect(() => {
    setSelectAreaKey(value)
  }, [value])
  return (
    <MultiCascader
      value={selectAreaKey}
      data={areaData}
      style={{ width: '100%' }}
      placeholder="请选择"
      renderValue={(a, b) => renderMultiCascaderTag(b)}
      locale={{ searchPlaceholder: '输入省市区名称' }}
      onChange={(v) => { onChange(v); setSelectAreaKey(v) }}
      {...rest}
    />
  )
}

export default AddressMultiCascader;

import AddressMultiCascader from '@/components/address-multi-cascader'

const [selectKeys, setSelectKeys] = useState([]);

   const getAreaData = (v) => {
      const arr = [];
      v?.forEach?.(item => {
        let deep = 0;
        let node = window.yeahgo_area.find(it => it.id === item);
        const nodeIds = [node.id];
        const nodeNames = [node.name]
        while (node.pid) {
          deep += 1;
          node = window.yeahgo_area.find(it => it.id === node.pid);
          nodeIds.push(node.id);
          nodeNames.push(node.name);
        }
        arr.push({
          provinceId: nodeIds[deep],
          cityId: deep > 0 ? nodeIds[deep - 1] : 0,
          districtId: deep > 1 ? nodeIds[deep - 2] : 0,
          areaName: nodeNames.reverse().join('|')
        })
      })
    
      return arr;
    }
    const getAreaDatas = (v) => {
      const arr = [];
      const brr = []
      v?.forEach?.(item => {
        let deep = 0;
        let node = window.yeahgo_area.find(it => it.id === item);
        const nodeIds = [node.id];
        const nodeNames = [node.name]
        if(node.children){
          const toTreeData = (data) => { 
            data?.forEach(item => {
              if(item.deep == 3){
                brr.push(item.id)
              }
                toTreeData(item.children)
            })  
          }
          toTreeData(node?.children)
        }
        while (node.pid) {//找父级
          deep += 1;
          node = window.yeahgo_area.find(it => it.id === node.pid);
          nodeIds.push(node.id);
          nodeNames.push(node.name);
        }
        arr.push({
          provinceId: nodeIds[deep],
          cityId: deep > 0 ? nodeIds[deep - 1] : 0,
          districtId: deep > 1 ? nodeIds[deep - 2] : 0,
          areaName: nodeNames.reverse().join('|')
        })
      })
    if(brr.length){
      return getAreaData(brr)
    }else{
      return arr;
    }

    }

<AddressMultiCascader
     value={selectKeys}
     placeholder="添加地区"
     renderValue={() => <span style={{ color: '#8e8e93' }}>添加地区</span>}
     renderExtraFooter={() =>
     <div style={{ padding: 10, textAlign: 'right' }}>
       <Button type="primary" onClick={() => { setArea() }}>确定</Button>
     </div>}
     onChange={setSelectKeys}
     disabledItemValues={disabledItemValues}
     onClose={() => { setSelectKeys([]) }}
/>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容