项目搭建
项目展示 傲娇的猪猪点击F12进入手机模式使用
源码
创建项目
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的文档进行修改
封装导航栏
- 将svg抽离成一个文件
- 将导航栏抽离成一个文件
- 将布局抽离成一个文件
制作三个页面的总体布局
- 封装为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
- 上下两个部分,上边是标签,下边是按钮
- 首先标签是通过遍历来展示,因为标签要可以编辑所以应该使用a标签,我使用link标签添加一个跳转路由跳转到单个的tag.tsx标签中进行编辑。
import {Link} from 'react-router-dom';
- 考虑到这个按钮要被多次使用我就直接进行了抽离,方便其他地方使用,新建另一个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}
- 其次还增加了两个辅助组件,center.tsx和space.tsx,前者是用来垂直居中后者是用来添加间距的。
- 功能的实现思路:通过我定义的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}
- 给标签赋默认值
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);
},[])
-
在useTags中将获取到的值进行存储存储到本地中,第一次更新不能update,不然第一次赋默认值会被更新一次。
-
为解决上边这个问题我重新写另一个在第二次更新用的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}
- 到此该页面的问题基本解决
制作tag.tsx
- 页面布局引入组件,主要内容有上下两部分,上边是一个可以修改的标签,下边使用定义好的按钮和对应hooks即可。
- 通过react路由中useParams来找到标签对应id,然后再通过id找到对应的标签名进行修改。
- 同样删除标签的功能也在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列表
- 这个的主要思路和之前的tag类似
- 标签也是可以点击的进入之后进行删除就可。
- 删除后点击下边导航栏返回。
- 主要代码
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,我只能使用时间最为唯一的删除依据。
总结
- 这个项目是一个极简的记账项目,主要是用来给自己用的。
- 项目中遇到了很多的问题,例如路由转换,项目该如何搭建,以及有些插件的问题
- 该项目主要是使用ts写的,因为ts语法比较严谨一些。
- 其中用到了react很多的知识,我还自己定义了三个hooks
- 对项目中的一些地方,我进行了多次的封装,保证代码的质量,减少重复,以便于后期维护,