React果果记账-记账页面功能搭建

473 阅读3分钟

TagsSection

import styled from "styled-components";
import React, {useState} from "react";

const Wrapper = styled.section`
  background: white;
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  flex-grow: 1;

  > ol {
    margin: 0 -12px;

    > li {
      background: #D9D9D9;
      border-radius: 18px;
      display: inline-block;
      padding: 3px 18px;
      font-size: 14px;
      margin: 8px 12px;

      &.selected {
        background: #ffd833;
      }
    }
  }

  > button {
    background: none;
    border: none;
    color: #666;
    padding: 2px 4px;
    margin-top: 8px;
  }
;

`
const TagsSection: React.FunctionComponent = () => {
    const [tags, setTags] = useState<string[]>(['衣', '食', '住', '行']);
    const [selectedTags, setSelectedTags] = useState<string[]>([]);
    const onAddTag = () => {
        const tagName = window.prompt('新标签名称为:');
        if (tagName !== null) {
            setTags([...tags, tagName]);
        }
    };
    const onToggleTag = (tag: string) => {
        const index = selectedTags.indexOf(tag);
        if (index >= 0) {
            setSelectedTags(selectedTags.filter(t => t !== tag));
            //如果tag已被选中,就复制所有没有被选中的tag,作为新的selectedTags
        } else {
            setSelectedTags([...selectedTags, tag])
        }
    }
    const getClass = (tag: string) => selectedTags.indexOf(tag) >= 0 ? 'selected' : '';
    return (
        <Wrapper>
            <ol>
                {tags.map(tag =>
                    <li key={tag} onClick={
                        () => {
                            onToggleTag(tag);
                        }
                    } className={getClass(tag)}>{tag}</li>
                )}
            </ol>
            <button onClick={onAddTag}>新增标签</button>
        </Wrapper>
    );
};


export {TagsSection}

NoteSection

受控组件写法

const NoteSection: React.FunctionComponent = () => {
    const [note, setNote] = useState('')
    console.log(note)
    return (
        <Wrapper>
            <label>
                <span>备注:</span>
                <input type="text"
                       placeholder="在这里添加备注"
                       value={note}
                       onChange={(e) => setNote(e.target.value)}/>
            </label>
        </Wrapper>
    )
}

非受控组件写法

const NoteSection: React.FunctionComponent = () => {
    const [note, setNote] = useState('');
    const refInput=useRef<HTMLInputElement>(null)
    const onBlur=()=>{
        if(refInput.current!==null){
            setNote(refInput.current.value)
        }
    }
    return (
        <Wrapper>
            <label>
                <span>备注:</span>
                <input type="text"
                       placeholder="在这里添加备注"
                       ref={refInput}
                       defaultValue={note}
                       onBlur={onBlur}/>
            </label>
        </Wrapper>
    )
}

React和HTML的onChange区别

  • React的onChange会在输入字符的时候触发
  • HTML的onChange是在鼠标移走后才会触发,早于onBlur

CategorySection

import styled from "styled-components";
import React, {useState} from "react";

const Wrapper = styled.section`
  font-size: 24px;

  > ul {
    display: flex;
    background: #c4c4c4;

    > li {
      width: 50%;
      text-align: center;
      padding: 16px 0;

      &.selected {
        background: #ffd833;
      }
    }
  }

`

const CategorySection: React.FunctionComponent = () => {
    const categoryMap = {'-': '支出', '+': '收入'}
    type Keys = keyof typeof categoryMap
    const [categoryList] = useState<Keys[]>(['-', '+'])
    //相当于const [categoryList] = useState<('-'|'+')[]>(['-', '+'])
    const [category, setCategory] = useState('-')
    return (
        <Wrapper>
            <ul>
                {categoryList.map(c =>
                    <li key={c}
                        className={category === c ? 'selected' : ''}
                        onClick={() => setCategory(c)}>
                        {categoryMap[c]}
                    </li>
                )}
            </ul>
        </Wrapper>
    )
}

export {CategorySection}

NumberPadSection

键盘输入功能

const NumberPadSection: React.FunctionComponent = () => {
    const [output, _setOutput] = useState<string>('0')
    const setOutput=(output:string)=>{
        //对setOutput进行封装
        if (output.length>16){
            output=output.slice(0,16);
            //最大长度16
        }else if (output.length===0){
            output='0'
        }
        _setOutput(output)
    }
    const onClickButtonWrapper = (e: React.MouseEvent) => {
        const text = (e.target as HTMLButtonElement).textContent
        //强制指定类型
        console.log(text)
        if (text === null) {
            return
        }
        if (text) {
            switch (text) {
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    if (output === '0') {
                        setOutput(text)
                    } else {
                        setOutput(output + text)
                    }
                    break;
                case '.':
                    if (output.indexOf('.')>=0){return;}
                    setOutput(output+'.')
                    break;
                case'删除':
                    if (output.length === 1) {
                        setOutput('')

                    } else {
                        setOutput(output.slice(0, -1))//等于output.slice(start:0,end:output-1)
                    }
                    break;
                case'清空':
                    setOutput('');
                    break;
                case'OK':
                    console.log('确认');
                    break;
            }
        }
    }
    return (
        <Wrapper>
            <div className="output">
                {output}
            </div>
            <div className="pad clearfix" onClick={onClickButtonWrapper}>
                <button>1</button>
                <button>2</button>
                <button>3</button>
                <button>删除</button>
                <button>4</button>
                <button>5</button>
                <button>6</button>
                <button>清空</button>
                <button>7</button>
                <button>8</button>
                <button>9</button>
                <button className="ok">OK</button>
                <button className="zero">0</button>
                <button>.</button>
            </div>
        </Wrapper>
    )
}

键盘输入功能代码优化-TyScript之联合字符串

  • 将Wrapper抽出为单独文件
  • 将switch函数抽出
    • setOutput改为return
    • 删除break
//type InputString = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | '删除' | '清空'
//后期可以强制设定text类型,使代码更加严s谨
//const generateOutput = (text: InputString, output = '0') => {
const generateOutput = (text: string, output = '0') => {
    switch (text) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            if (output === '0') {
                return text;
            } else {
                return output + text;
            }
        case '.':
            if (output.indexOf('.') >= 0) {
                return output;
            }
            return output + '.';
        case'删除':
            if (output.length === 1) {
                return '';

            } else {
                return output.slice(0, -1)||''//等于output.slice(start:0,end:output-1),或运算防止undefined
            }
        case'清空':
            return '';
        default:
            return '';
    }
}

export {generateOutput}
  • 主函数变为
const onClickButtonWrapper = (e: React.MouseEvent) => {
    const text = (e.target as HTMLButtonElement).textContent
    //强制指定类型
    console.log(text)
    if (text === null) {
        return
    }
    if (text==='OK') {
        return;
    }
    if('0123456789.'.split('').concat(['删除','清空']).indexOf(text)>=0){
        setOutput(generateOutput(text,output));
    }
}

将四部分组件统筹到Money.tsx

import Layout from "../components/Layout";
import React, {useState} from "react";
import styled from "styled-components";
import {TagsSection} from "./money/TagsSection";
import {NoteSection} from "./money/NoteSection";
import {CategorySection} from "./money/CategorySection";
import {NumberPadSection} from "./money/NumberPadSection";

const MyLayout = styled(Layout)`
  display: flex;
  flex-direction: column;

`
type Category = '-' | '+'

function Money() {
    const [selected, setSelected] = useState({
        tags: [] as string[],
        note: '',
        category: '-' as Category,
        amount: 0
    })
    return (
        <MyLayout>
            {selected.tags.join(',')}
            <hr/>
            {selected.note}
            <hr/>
            {selected.category}
            <hr/>
            {selected.amount}
            <TagsSection value={selected.tags}
                         onChange={(tags) => setSelected({...selected, tags: tags})}/>
            <NoteSection value={selected.note}
                         onChange={(note) => setSelected({...selected, note: note})}/>
            <CategorySection value={selected.category}
                             onChange={(category) => setSelected({...selected, category: category})}/>
            <NumberPadSection value={selected.amount}
                              onChange={(amount) => setSelected({...selected, amount: amount})}
                              onOk={() => {
                              }}/>
        </MyLayout>
    )
}

export default Money;

注意:每个组件都需要进行对应修改,因为组建内的内部数据变成外部数据了

代码重构

  • 封装onChange,简化代码
import Layout from "../components/Layout";
import React, {useState} from "react";
import styled from "styled-components";
import {TagsSection} from "./money/TagsSection";
import {NoteSection} from "./money/NoteSection";
import {CategorySection} from "./money/CategorySection";
import {NumberPadSection} from "./money/NumberPadSection";

const MyLayout = styled(Layout)`
  display: flex;
  flex-direction: column;

`
type Category = '-' | '+'

function Money() {
    const [selected, setSelected] = useState({
        tags: [] as string[],
        note: '',
        category: '-' as Category,
        amount: 0
    })
    const onChange=(obj:Partial<typeof selected>)=>{//obj是selected的部分类型,用typeof获取值(selected)的类型
        setSelected({...selected, ...obj})
    }
    return (
        <MyLayout>
            <TagsSection value={selected.tags}
                         onChange={(tags) => onChange({tags})}/>
            <NoteSection value={selected.note}
                         onChange={(note) => onChange({note})}/>
            <CategorySection value={selected.category}
                             onChange={(category) => onChange({category})}/>
            <NumberPadSection value={selected.amount}
                              onChange={(amount) => onChange({amount})}
                              onOk={() => {
                              }}/>
        </MyLayout>
    )
}

export default Money;