傲娇的猪猪(记账项目)react

271 阅读7分钟

项目搭建

项目展示 傲娇的猪猪点击F12进入手机模式使用扫码

源码

logo

创建项目

yarnglobal add create-react-app
create-react-app money--template typescript
cd money
yarn start

cra的官网有好东西可以看看官网

.gitignore

这东西主要是把不用的东西也就是个人偏好不要放仓库

css相关的一些配置

css normalize

cra官网有主要看adding css reset这一章节

scss的支持

  • 使用dart-sass代替node-sass
  • yarn add --dev node-sass@npm:dart-sass 很多时候都是谷歌找答案,耐心就好了,总有人和你问题一样

让css@import引用更加方便

  • Vue项目使用@表示src/目录
  • react不一样 可以直接@import 'sss/bbb'
  • JS需要在tsconfig.json添加一行"baseUrl":'src' 我也是下文档中找到的cra文档

这次使用的是css-in-js方案:styled-components

  • 处理安装styled-components还要安装@types/styled-components 跟着官网学就好了

1. 开始先制作底部的导航栏

安装react-router-dom

  • 命令:yarn add react-router-dom
  • 根据提示安装ts申明文件
  • yarn add --dev @types/react-router-dom
  • 可以谷歌搜React-router进入官方文档

确定url

  • #/money 记账
  • #/label 标签
  • #/statistics 统计
  • 添加404页面
  • 默认在记账

使用svg symbols

使用这个需要自己定义一下webpack config,首先yarn eject拿到webpack配置,一定要先提交代码,然后根据svg-sprite-loader的文档进行修改

封装导航栏

  1. 将svg抽离成一个文件
  2. 将导航栏抽离成一个文件
  3. 将布局抽离成一个文件

制作三个页面的总体布局

  1. 封装为layout.tsx文件
<Wrapper>
        <Main ref={mainRef} className={props.className} >
            {props.children}
        </Main>
        <Nav/>
    </Wrapper>

2. 制作money.tsx

  • 具体看源码
  • 从上到下分为四个部分
  • 第四个部分有可以分两个部分
  • 文件目录如下,这个是我封装后的样子,之前在money.tsx文件中,后来注意到文件代码过多,我就抽离了
  • 现在money.tsx是这样子的:引入四个组件就可以了
  • 使用即可
              <TagsSection value = {selected.tagIds}
                         onChange={ (tagIds)=>onChange({tagIds})}/>

              <NoteSection value={selected.note}
                         onChange={(note)=>onChange({note})}/>
  
              <CategorySelection value={selected.category}
                                   onChange={(category)=>onChange({category})}/

              <NumberPadSection value={selected.amount}
                              onChange={(amount)=>onChange({amount})}
                              onOk={submit}/>
  • 关于功能的实现思路是这样 的,每个组件收集数据传入money.tsx,然后在money.tsx中监听,然后将收集到的各个数据放入一个对象中,利用onOk来将数据提交至useRecords,本来是放在这个文件里的,但是后来发现不方便,(之后我将收集的数据放入我定义的一个hooks 文件useRecords.tsx中进行存放,并且添加了一些功能,用来更新删除等。)部分代码如下:

const defaultFormDate ={
    tagIds: [] as number[],
        note:'',
    category:'-'  as Category,
    amount:0

}
//收录值
type Category = '-'|'+';
function Money() {
    const [selected,setSelected] = useState(defaultFormDate)
    const onChange=(obj:Partial<typeof selected>)=>{//把部分值传入
        setSelected({
            ...selected,
            ...obj
        });
    };
    
    //提交
    const {addRecords} =useRecords()

    const submit = ()=>{
        if(addRecords(selected)){
            alert('保存了')
            setSelected(defaultFormDate);
        }

    }

3. 制作tags.tsx

  • 上下两个部分,上边是标签,下边是按钮
  1. 首先标签是通过遍历来展示,因为标签要可以编辑所以应该使用a标签,我使用link标签添加一个跳转路由跳转到单个的tag.tsx标签中进行编辑。
import {Link} from 'react-router-dom';
  1. 考虑到这个按钮要被多次使用我就直接进行了抽离,方便其他地方使用,新建另一个Button.tsx组件
  • 代码如下
import styled from 'styled-components';

const Button = styled.button`
font-size: 18px;
border: none;
padding: 8px 12px;
background:#767676;
border-radius: 4px;
color: white;
&:hover{
background: red;
}
`
export {Button}
  1. 其次还增加了两个辅助组件,center.tsx和space.tsx,前者是用来垂直居中后者是用来添加间距的。
  2. 功能的实现思路:通过我定义的hooks文件 useTags.tsx来进行取值便利和添加标签,在这个文件中我添加了很多个方法,有些是后来添加上去的,随着开发的需求进行添加,至于命名还是比较规范的: tags, setTags, findTag, updateTag, findTagIndex, deleteTag, onAddTag, getName
  • useTags.tsx代码如下
port {useEffect, useState} from 'react';
import {createId} from '../lib/createId';
import {useUpdate} from './useUpdate';

const useTags= ()=>{
    const [tags,setTags] = useState<{id:number,name:string}[]>([])
    const findTag = (id:number)=>tags.filter(tag=>tag.id===id)[0]
    const findTagIndex = (id:number)=>{
        let result = -1;
        for(let i = 0;i<tags.length;i++){
            if(tags[i].id===id){
                result=i;
                break;
            }
        }
         return result;

    }
    //这个是之前写的代码比较垃圾,后来优化了,在下边
    // const updateTag = (id:number,obj:{name:string})=>{
    //     const index = findTagIndex(id);
    //     const tagsClone = JSON.parse( JSON.stringify(tags));
    //     tagsClone.splice(index,1,{id:id,name:obj.name})//返回值是被删的对象
    //     setTags(tagsClone)
    // }
    // const deleteTag = (id:number)=>{
    //     const index = findTagIndex(id);
    //     const tagsClone = JSON.parse( JSON.stringify(tags));
    //     tagsClone.splice(index,1)
    //     setTags(tagsClone)
    // }
    //优化

    useEffect(()=>{
       let localTags =  JSON.parse(window.localStorage.getItem('tags')||'[]')
        if(localTags.length===0){
            localTags = [
                {id:createId(),name:'衣'},
                {id:createId(),name:'食'},
                {id:createId(),name:'住'},
                {id:createId(),name:'行'},
            ]
        }
        setTags(localTags);
    },[])
    useUpdate(()=>{
        window.localStorage.setItem('tags',JSON.stringify(tags))//不包含第一次更新
    },tags)
    const updateTag = (id:number,{name}:{name:string})=> {
        setTags(tags.map(tag=>{
            return tag.id === id? {id,name} : tag;
        }))
    }
    const deleteTag = (id:number)=>{
        setTags(tags.filter(tag=>tag.id!==id))//创建新的数组

    }
    const onAddTag = ()=>{
        const tagName = window.prompt('输入您要添加的标签');
        if(tagName!==null && tagName!==''){
            setTags([...tags,{id: createId(),name:tagName}])
        }
    }
    const getName = (id:number)=>{
    const tag =   tags.filter(t=>t.id===id)[0]
        return tag? tag.name:'标签被删除';
}
    return{
        tags,
        setTags,
        findTag,
        updateTag,
        findTagIndex,
        deleteTag,
        onAddTag,
        getName
    }
}
export {useTags}

  1. 给标签赋默认值
 useEffect(()=>{
       let localTags =  JSON.parse(window.localStorage.getItem('tags')||'[]')
        if(localTags.length===0){
            localTags = [
                {id:createId(),name:'衣'},
                {id:createId(),name:'食'},
                {id:createId(),name:'住'},
                {id:createId(),name:'行'},
            ]
        }
        setTags(localTags);
    },[])
  1. 在useTags中将获取到的值进行存储存储到本地中,第一次更新不能update,不然第一次赋默认值会被更新一次。

  2. 为解决上边这个问题我重新写另一个在第二次更新用的updatetags.tsx.

import {useEffect, useRef} from 'react';

const useUpdate = (fn:()=>void,deps:any[])=>{
    const count =useRef(0)
    useEffect(()=>{
        count.current +=1;
    })
    useEffect(()=>{
        if(count.current>1) {
            fn();
        }

    },[fn,deps])//每次产生一个新的tags才会触发
}
export {useUpdate}
  1. 到此该页面的问题基本解决

制作tag.tsx

  • 页面布局引入组件,主要内容有上下两部分,上边是一个可以修改的标签,下边使用定义好的按钮和对应hooks即可。
  1. 通过react路由中useParams来找到标签对应id,然后再通过id找到对应的标签名进行修改。
  2. 同样删除标签的功能也在useTags中定义了,直接引用。
const Tag: React.FC =()=>{
    const {findTag,updateTag,deleteTag} = useTags()
      let { id}= useParams<Params>()//把id命名为idString
    const tag =findTag(parseInt(id))
    const history = useHistory()
    const onclickBack=()=>{
       history.goBack()

4. 制作statistics.tsx

  • 该页面主要是由上边切换导航栏和下边主要内容组成
  • 下边的主要内容分为一个h标签和ol列表
  1. 这个的主要思路和之前的tag类似
  2. 标签也是可以点击的进入之后进行删除就可。
  3. 删除后点击下边导航栏返回。
  • 主要代码
type RecordItem ={
    tagIds: number[];
    note:string;
    category:'+'|'-';
    amount: number;
    createdAt:string;
}
function Statistics() {
    const [category,setCategory] = useState<'-'|'+'>('-')
    const {records} = useRecords();
    const {getName} = useTags();
    const selectedRecords = records.filter(r=>r.category===category)//选中的category
    const hash:{ [K:string]:RecordItem[] } ={}
    selectedRecords.forEach(r=>{
        const key =dayjs(r.createdAt).format('YYYY-MM-DD');
        if(!(key in hash)){
            hash[key] = [];
        }
        hash[key].push(r)
    });

    const array = Object.entries(hash).sort((a,b)=>{

        if(a[0]===b[0]) return 0;
        if(a[0]>b[0]) return -1;
        if(a[0]<b[0]) return 1;
        return 0;
    })
    return(
        <Layout>
            <CategoryWrapper>
                <CategorySelection value={category}
                                   onChange={value => setCategory(value)}/>

            </CategoryWrapper>
            {array.map(([date,records])=><div key={date}>
                <Header>{date}</Header>
                <div>
                    {records.map(r=>{
                        return <Item key={r.createdAt}>
                            <Link to={"/Statistics/"+r.tagIds[0]} className="tags">
                                <Name>
                                    {r.tagIds.map(t=><span key={r.tagIds[0]} > {getName(t)}</span>)}
                                </Name>

                            </Link>

                            { r.note&&<div className="note">
                                {r.note}
                            </div>}
                            <div className="amount">
                                ¥{r.amount}
                            </div>

                            {/*{dayjs(r.createdAt).format('YYYY年MM月DD日')}*/}

                        </Item>
                    })}
                </div>
            </div>)}

        </Layout>


    )
}
  • 定义了一个哈希对象,把同一组的数据放在哈希对象的一个数组中,在下边li列表中进行展示,首先选哟定义一个哈希,给他定义类型,然后便利哈希表,将日期作为哈希表的键值,每一个键值对应一个存储日期的组,一组日期就是一天。最后将排好序的哈希表遍历展示即可。同样的操作用的方法在之前的hooks中进行定义调用。

制作statistic

  • 这个页面比较简单,对点击的记录进行删除就好了
  • 代码如下
const Statistic:React.FC = ()=> {
    const {findRecords, deleteRecords} = useRecords()
    let {id} = useParams<Params>()//把id命名为idString
    const tag = findRecords(parseInt(id))
    const history = useHistory()
    const onclickBack = () => {
        history.goBack()
    }

    if (tag) {
        return (
            <Layout>
                <Topbar>
                    <Icon name='left' onClick={onclickBack}/>
                    <span>编辑标签</span>
                    <Icon/>
                </Topbar>
                <InputWrapper>
                    确定要删除点击记账记录?
                </InputWrapper>

                <Center>
                    <Space/>
                    <Space/>
                    <Space/>
                    <Space/>
                    <Space/>
                    <Button onClick={() => {
                        deleteRecords(tag.createdAt)
                    }}>删除标签</Button>
                </Center>

            </Layout>

        )
    } else {
        return (
            <Layout>
                <Center>
                    标签已删除,点击下方导航栏返回
                </Center>

            </Layout>

        )
    }
}

  • 和tag一样利用react-rounter获取对应标签的id然后在useRecords中添加一个删除方法,调用删除对应的记录即可,由于我没有设计唯一的id,我只能使用时间最为唯一的删除依据。

总结

  1. 这个项目是一个极简的记账项目,主要是用来给自己用的。
  2. 项目中遇到了很多的问题,例如路由转换,项目该如何搭建,以及有些插件的问题
  3. 该项目主要是使用ts写的,因为ts语法比较严谨一些。
  4. 其中用到了react很多的知识,我还自己定义了三个hooks
  5. 对项目中的一些地方,我进行了多次的封装,保证代码的质量,减少重复,以便于后期维护,