React TODO | 青训营

88 阅读3分钟

学习了React后,总得实践一下不是。废话不多说,先上效果!

github.com/SuperKenVer…

(掘金什么垃圾不能传视频他妈的)

图片.png

(他妈的还加水印)

图片.png

图片.png

图片.png

我接下来从以下几点介绍我这个项目:

  • 糊UI
  • 做动画/效果
  • 持久存储

特别感谢Generative Pretrained Transformer,帮了我很多!

糊UI

以前我用过SwiftUI和Jetpack Compose,布局主要靠HStack VStack Column Row啥的,而且我还没接触过css。到了React里了,得写css啊!咋办呢?

调研(指问GPT)一番后,我决定使用grid来布局。

图片.png

就,像这样把网格划好

.todo-item {
  display: grid;
  grid-template-columns: 50px auto;
  grid-template-rows: auto auto;
}

然后把东西塞进去(省略了部分css):

interface TodoItemArgs {
  title: string
  content: string
  done: boolean
  setDone: (done: boolean) => void
}

const TodoItem: React.FC<TodoItemArgs> = ({ title , content, done, setDone }) => {
  
  return (
    <>
      <div className="todo-item card" onClick={() => {
        setDone(!done)
      }}>
        <div className='todo-checkbox'>
          <CheckBox done={done} />
        </div>
        <h3 className='todo-title'>{title}</h3>
        <p className='todo-content'>{content}</p>
      </div>
    </>
  )
}

interface CheckboxArgs {
  done: boolean
}

const CheckBox: React.FC<CheckboxArgs> = ({done}) => {
  const r=10
  const border=2

  const dark=useMediaQuery('(prefers-color-scheme: dark)')
  const light=!dark

  // const foreground=light?"black":"white"
  // const background=light?"white":"black"
  return (
    <>
      <svg width={2*(r+border)} height={2*(r+border)}>
        <circle r={r} cx={r+border} cy={r+border} 
          fill={done ? "#11DD11":"transparent"} 
          stroke={light?"#AAAAAA":"#AAAAAA"} stroke-width={border} 
        />
      </svg>
    </>
  )
}

.todo-checkbox {
  grid-column: 1/2;
  grid-row: 1/3;
}

.todo-title {
  grid-column: 2;
  grid-row: 1;
}

.todo-content {
  grid-column: 2;
  grid-row: 2;
}

其中,那个标记东西完没完成的小圈,是用svg写的。我挺不喜欢通过css奇技淫巧来画东西的,什么通过border+旋转实现一个勾这种东西,就让人很没有安全感,要稍微画复杂一点就画不出来了,而且还不好阅读。所以我选择了svg。

然后,东西就被塞进去了(废话)

再加一些全局的居中之类的代码,

#root {
  text-align: center;
  margin: 0 auto;
  padding: 2rem;
}

差不多就把UI糊好了

动画和效果

从github的视频和上面的截图中可以看到,我为每个待办事项添加了阴影,而且鼠标经过的时候还会放大、阴影更深。这让画面好看了许多(自豪.jpg)。

阴影

这个其实非常简单,就一行css搞定了

box-shadow: 2px 2px 12px rgba(0,0,0,0.2);

圆角边框

一样简单

border-radius: 10px;

鼠标经过时调整

这里就是:

  1. 把阴影加深一点
  2. 让卡片变大一点

这样就让它像被抬起来了一样~

.card:hover {
  box-shadow: 5px 5px 20px rgba(0,0,0,0.5);
  transform: scale(1.05);
}

动画

阴影的话,我就直接选择了ease-out,先快后慢,挺舒服的。

放大的话,我想让他Q弹一点,于是只好选择使用贝塞尔曲线,让它先放大过头一点再弹回来,效果可好了!(不要脸+1)

用Firefox自带的工具调的曲线,感觉挺好用的。 图片.png

就是web标准怎么不允许多几个锚点的曲线呢,,,,,,,,,……虽然我也可以不用

  transition: box-shadow 0.15s ease-out,
              transform 0.15s cubic-bezier(0,0,0.76,1.73);

持久化存储

偷了个懒,直接localStorage了,没咋优化性能,所有待办事项全部存一个位置。

然后,React的state啥的还是要用起来,别破坏别人提供的抽象嘛

所以,最后大致是这样实现的:

const Component: React.FC<ArgType> = () => {
    const [ todos, setTodos ] = useState(
        () => JSON.parse(localStorage.getItem("todos") ?? "[]" )
    )
    useEffect( () => {
        localStorage.setItem("todos",JSON.stringify(todos)
    }, [todos])
    
    return (
        //...
    )

其中,往todos添加事项的代码不能这么写:

todos.push(todo)
setTodos(todos)

可能是因为React判断两个todos是同一个对象,所以不会执行todos变化时该干的活,这个push等于是绕过了React。能跑的写法是这样的:

let new_todos=[...todos]  // Make a copy of todos
new_todos.push(todo)
setTodos(new_todos)

要复制一份,React才会认为他们是两个不同的对象,才会重绘界面。