zheng 2 tuần trước cách đây
mục cha
commit
08ed917eac

+ 338 - 146
21.react/高阶/project2/src/App.css

@@ -1,184 +1,376 @@
-.counter {
-  font-size: 16px;
-  padding: 5px 10px;
-  border-radius: 5px;
-  color: var(--accent);
-  background: var(--accent-bg);
-  border: 2px solid transparent;
-  transition: border-color 0.3s;
-  margin-bottom: 24px;
-
-  &:hover {
-    border-color: var(--accent-border);
-  }
-  &:focus-visible {
-    outline: 2px solid var(--accent);
-    outline-offset: 2px;
-  }
+html,
+body {
+  margin: 0;
+  padding: 0;
 }
 
-.hero {
-  position: relative;
+button {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  background: none;
+  font-size: 100%;
+  vertical-align: baseline;
+  font-family: inherit;
+  font-weight: inherit;
+  color: inherit;
+  -webkit-appearance: none;
+  appearance: none;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
 
-  .base,
-  .framework,
-  .vite {
-    inset-inline: 0;
-    margin: 0 auto;
-  }
+body {
+  font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  line-height: 1.4em;
+  background: #f5f5f5;
+  color: #4d4d4d;
+  min-width: 230px;
+  max-width: 550px;
+  margin: 0 auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-weight: 300;
+}
 
-  .base {
-    width: 170px;
-    position: relative;
-    z-index: 0;
-  }
+:focus {
+  outline: 0;
+}
 
-  .framework,
-  .vite {
-    position: absolute;
-  }
+.hidden {
+  display: none;
+}
 
-  .framework {
-    z-index: 1;
-    top: 34px;
-    height: 28px;
-    transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
-      scale(1.4);
-  }
+.todoapp {
+  background: #fff;
+  margin: 130px 0 40px 0;
+  position: relative;
+  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
 
-  .vite {
-    z-index: 0;
-    top: 107px;
-    height: 26px;
-    width: auto;
-    transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
-      scale(0.8);
-  }
+.todoapp input::-webkit-input-placeholder {
+  font-style: italic;
+  font-weight: 300;
+  color: #e6e6e6;
 }
 
-#center {
-  display: flex;
-  flex-direction: column;
-  gap: 25px;
-  place-content: center;
-  place-items: center;
-  flex-grow: 1;
+.todoapp input::-moz-placeholder {
+  font-style: italic;
+  font-weight: 300;
+  color: #e6e6e6;
+}
 
-  @media (max-width: 1024px) {
-    padding: 32px 20px 24px;
-    gap: 18px;
-  }
+.todoapp input::input-placeholder {
+  font-style: italic;
+  font-weight: 300;
+  color: #e6e6e6;
 }
 
-#next-steps {
-  display: flex;
-  border-top: 1px solid var(--border);
-  text-align: left;
+.todoapp h1 {
+  position: absolute;
+  top: -155px;
+  width: 100%;
+  font-size: 100px;
+  font-weight: 100;
+  text-align: center;
+  color: rgba(175, 47, 47, 0.15);
+  -webkit-text-rendering: optimizeLegibility;
+  -moz-text-rendering: optimizeLegibility;
+  text-rendering: optimizeLegibility;
+}
 
-  & > div {
-    flex: 1 1 0;
-    padding: 32px;
-    @media (max-width: 1024px) {
-      padding: 24px 20px;
-    }
-  }
+.new-todo,
+.edit {
+  position: relative;
+  margin: 0;
+  width: 100%;
+  font-size: 24px;
+  font-family: inherit;
+  font-weight: inherit;
+  line-height: 1.4em;
+  border: 0;
+  color: inherit;
+  padding: 6px;
+  border: 1px solid #999;
+  box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+  box-sizing: border-box;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
 
-  .icon {
-    margin-bottom: 16px;
-    width: 22px;
-    height: 22px;
-  }
+.new-todo {
+  padding: 16px 16px 16px 60px;
+  border: none;
+  background: rgba(0, 0, 0, 0.003);
+  box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+}
 
-  @media (max-width: 1024px) {
-    flex-direction: column;
-    text-align: center;
-  }
+.main {
+  position: relative;
+  z-index: 2;
+  border-top: 1px solid #e6e6e6;
 }
 
-#docs {
-  border-right: 1px solid var(--border);
+.toggle-all {
+  width: 1px;
+  height: 1px;
+  border: none; /* Mobile Safari */
+  opacity: 0;
+  position: absolute;
+  right: 100%;
+  bottom: 100%;
+}
 
-  @media (max-width: 1024px) {
-    border-right: none;
-    border-bottom: 1px solid var(--border);
-  }
+.toggle-all + label {
+  width: 60px;
+  height: 34px;
+  font-size: 0;
+  position: absolute;
+  top: -52px;
+  left: -13px;
+  -webkit-transform: rotate(90deg);
+  transform: rotate(90deg);
 }
 
-#next-steps ul {
+.toggle-all + label:before {
+  content: '❯';
+  font-size: 22px;
+  color: #e6e6e6;
+  padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked + label:before {
+  color: #737373;
+}
+
+.todo-list {
+  margin: 0;
+  padding: 0;
   list-style: none;
+}
+
+.todo-list li {
+  position: relative;
+  font-size: 24px;
+  border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+  border-bottom: none;
+}
+
+.todo-list li.editing {
+  border-bottom: none;
   padding: 0;
-  display: flex;
-  gap: 8px;
-  margin: 32px 0 0;
+}
 
-  .logo {
-    height: 18px;
-  }
+.todo-list li.editing .edit {
+  display: block;
+  width: calc(100% - 43px);
+  padding: 12px 16px;
+  margin: 0 0 0 43px;
+}
 
-  a {
-    color: var(--text-h);
-    font-size: 16px;
-    border-radius: 6px;
-    background: var(--social-bg);
-    display: flex;
-    padding: 6px 12px;
-    align-items: center;
-    gap: 8px;
-    text-decoration: none;
-    transition: box-shadow 0.3s;
-
-    &:hover {
-      box-shadow: var(--shadow);
-    }
-    .button-icon {
-      height: 18px;
-      width: 18px;
-    }
-  }
+.todo-list li.editing .view {
+  display: none;
+}
 
-  @media (max-width: 1024px) {
-    margin-top: 20px;
-    flex-wrap: wrap;
-    justify-content: center;
+.todo-list li .toggle {
+  text-align: center;
+  width: 40px;
+  /* auto, since non-WebKit browsers doesn't support input styling */
+  height: auto;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  margin: auto 0;
+  border: none; /* Mobile Safari */
+  -webkit-appearance: none;
+  appearance: none;
+}
 
-    li {
-      flex: 1 1 calc(50% - 8px);
-    }
+.todo-list li .toggle {
+  opacity: 0;
+}
 
-    a {
-      width: 100%;
-      justify-content: center;
-      box-sizing: border-box;
-    }
-  }
+.todo-list li .toggle + label {
+  /*
+		Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
+		IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
+	*/
+  background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
+  background-repeat: no-repeat;
+  background-position: center left;
 }
 
-#spacer {
-  height: 88px;
-  border-top: 1px solid var(--border);
-  @media (max-width: 1024px) {
-    height: 48px;
-  }
+.todo-list li .toggle:checked + label {
+  background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
 }
 
-.ticks {
+.todo-list li label {
+  word-break: break-all;
+  padding: 15px 15px 15px 60px;
+  display: block;
+  line-height: 1.2;
+  transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+  color: #d9d9d9;
+  text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+  display: none;
+  position: absolute;
+  top: 0;
+  right: 10px;
+  bottom: 0;
+  width: 40px;
+  height: 40px;
+  margin: auto 0;
+  font-size: 30px;
+  color: #cc9a9a;
+  margin-bottom: 11px;
+  transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+  color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+  content: '×';
+}
+
+.todo-list li:hover .destroy {
+  display: block;
+}
+
+.todo-list li .edit {
+  display: none;
+}
+
+.todo-list li.editing:last-child {
+  margin-bottom: -1px;
+}
+
+.footer {
+  color: #777;
+  padding: 10px 15px;
+  height: 20px;
+  text-align: center;
+  border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+  content: '';
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  height: 50px;
+  overflow: hidden;
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
+    0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
+    0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+  float: left;
+  text-align: left;
+}
+
+.todo-count strong {
+  font-weight: 300;
+}
+
+.filters {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+  position: absolute;
+  right: 0;
+  left: 0;
+}
+
+.filters li {
+  display: inline;
+}
+
+.filters li a {
+  color: inherit;
+  margin: 3px;
+  padding: 3px 7px;
+  text-decoration: none;
+  border: 1px solid transparent;
+  border-radius: 3px;
+}
+
+.filters li a:hover {
+  border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+  border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+  float: right;
   position: relative;
-  width: 100%;
+  line-height: 20px;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.clear-completed:hover {
+  text-decoration: underline;
+}
+
+.info {
+  margin: 65px auto 0;
+  color: #bfbfbf;
+  font-size: 10px;
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+  text-align: center;
+}
+
+.info p {
+  line-height: 1;
+}
+
+.info a {
+  color: inherit;
+  text-decoration: none;
+  font-weight: 400;
+}
 
-  &::before,
-  &::after {
-    content: '';
-    position: absolute;
-    top: -4.5px;
-    border: 5px solid transparent;
+.info a:hover {
+  text-decoration: underline;
+}
+
+/*
+	Hack to remove background from Mobile Safari.
+	Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+  .toggle-all,
+  .todo-list li .toggle {
+    background: none;
+  }
+
+  .todo-list li .toggle {
+    height: 40px;
   }
+}
 
-  &::before {
-    left: 0;
-    border-left-color: var(--border);
+@media (max-width: 430px) {
+  .footer {
+    height: 50px;
   }
-  &::after {
-    right: 0;
-    border-right-color: var(--border);
+
+  .filters {
+    bottom: 10px;
   }
 }

+ 0 - 122
21.react/高阶/project2/src/App.tsx

@@ -1,122 +0,0 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from './assets/vite.svg'
-import heroImg from './assets/hero.png'
-import './App.css'
-
-function App() {
-  const [count, setCount] = useState(0)
-
-  return (
-    <>
-      <section id="center">
-        <div className="hero">
-          <img src={heroImg} className="base" width="170" height="179" alt="" />
-          <img src={reactLogo} className="framework" alt="React logo" />
-          <img src={viteLogo} className="vite" alt="Vite logo" />
-        </div>
-        <div>
-          <h1>Get started</h1>
-          <p>
-            Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
-          </p>
-        </div>
-        <button
-          type="button"
-          className="counter"
-          onClick={() => setCount((count) => count + 1)}
-        >
-          Count is {count}
-        </button>
-      </section>
-
-      <div className="ticks"></div>
-
-      <section id="next-steps">
-        <div id="docs">
-          <svg className="icon" role="presentation" aria-hidden="true">
-            <use href="/icons.svg#documentation-icon"></use>
-          </svg>
-          <h2>Documentation</h2>
-          <p>Your questions, answered</p>
-          <ul>
-            <li>
-              <a href="https://vite.dev/" target="_blank">
-                <img className="logo" src={viteLogo} alt="" />
-                Explore Vite
-              </a>
-            </li>
-            <li>
-              <a href="https://react.dev/" target="_blank">
-                <img className="button-icon" src={reactLogo} alt="" />
-                Learn more
-              </a>
-            </li>
-          </ul>
-        </div>
-        <div id="social">
-          <svg className="icon" role="presentation" aria-hidden="true">
-            <use href="/icons.svg#social-icon"></use>
-          </svg>
-          <h2>Connect with us</h2>
-          <p>Join the Vite community</p>
-          <ul>
-            <li>
-              <a href="https://github.com/vitejs/vite" target="_blank">
-                <svg
-                  className="button-icon"
-                  role="presentation"
-                  aria-hidden="true"
-                >
-                  <use href="/icons.svg#github-icon"></use>
-                </svg>
-                GitHub
-              </a>
-            </li>
-            <li>
-              <a href="https://chat.vite.dev/" target="_blank">
-                <svg
-                  className="button-icon"
-                  role="presentation"
-                  aria-hidden="true"
-                >
-                  <use href="/icons.svg#discord-icon"></use>
-                </svg>
-                Discord
-              </a>
-            </li>
-            <li>
-              <a href="https://x.com/vite_js" target="_blank">
-                <svg
-                  className="button-icon"
-                  role="presentation"
-                  aria-hidden="true"
-                >
-                  <use href="/icons.svg#x-icon"></use>
-                </svg>
-                X.com
-              </a>
-            </li>
-            <li>
-              <a href="https://bsky.app/profile/vite.dev" target="_blank">
-                <svg
-                  className="button-icon"
-                  role="presentation"
-                  aria-hidden="true"
-                >
-                  <use href="/icons.svg#bluesky-icon"></use>
-                </svg>
-                Bluesky
-              </a>
-            </li>
-          </ul>
-        </div>
-      </section>
-
-      <div className="ticks"></div>
-      <section id="spacer"></section>
-    </>
-  )
-}
-
-export default App

+ 1 - 1
21.react/高阶/project2/src/main.tsx

@@ -1,6 +1,6 @@
 import { StrictMode } from 'react'
 import { createRoot } from 'react-dom/client'
-import './index.css'
+// import './index.css'
 import App from './App.tsx'
 
 createRoot(document.getElementById('root')!).render(

+ 4 - 1
21.react/高阶/project3/src/App.jsx

@@ -7,6 +7,9 @@ import UseLearnCallBack from './components/UseLearnCallBack'
 import UseDemo1 from './components/UseDemo1'
 import UseLearnContext from './components/UseLearnContext'
 import UseLearnRef from './components/UseLearnRef'
+import UseLearnImperativeHandle from './components/UseLearnImperativeHandle'
+import Use from './components/Use'
+import UseLearnOptimistic from './components/UseLearnOptimistic'
 import { useState } from 'react'
 
 
@@ -15,7 +18,7 @@ function App() {
   return (
     <>
       <h1>首页</h1>
-      <UseLearnRef />
+      <UseLearnOptimistic />
       {/* <UseLearnMemo /> */}
       {/* {isShow ? <UseLearnEffect /> : '无内容'} */}
       {/* {isShow ? <UseDemo /> : '无内容'} */}

+ 91 - 0
21.react/高阶/project3/src/components/Use.jsx

@@ -0,0 +1,91 @@
+import { useEffect, useState } from "react";
+
+import { use, Suspense, useContext, createContext } from "react";
+
+const ThemeContext = createContext("light");
+function Demo1({ useId }) {
+
+    // const [loading, setLoading] = useState(true)
+    // const [error, setError] = useState(null)
+    // useEffect(() => {
+    //     // 访问一个接口 如果访问成功 显示用户信息 反之显示错误信息 进入页面展示loading 数据出现 loading消失
+    //     fetch(`/api/user/${useId}`)
+    //         .then((res) => {
+    //             if (res.ok) {
+    //                 setLoading(false)
+    //             }
+    //         })
+    //         .catch(err => console.log(err, 'err'))
+    // }, [])
+    // if (loading) return <div>加载中...</div>
+    // if (error) return <div>错误信息...</div>
+    // return (
+    //     <div>
+    //         <h1>一个小demo</h1>
+    //         <h2>{useId}</h2>
+    //     </div>
+    // )
+
+
+    // use() + Suspense 实现
+    // async function fetchUser(ids) {
+    //     const res = await fetch(`/api/user/${ids}`)
+    //     if (!res.ok) throw new Error('网络错误')
+    //     return res;
+    // }
+
+    // function UserMain({ useId }) {
+    //     const user = use(fetchUser(useId));
+    //     return <div>成功{user.status}号</div>
+    // }
+
+    // return (
+    //     <Suspense fallback={<div>加载中</div>}>
+    //         <UserMain useId={10}></UserMain>
+    //     </Suspense>
+    // )
+
+    // useContext
+
+}
+function ThemeBox() {
+    const [theme, setTheme] = useState('light')
+    const [isShow, setIsShow] = useState(true)
+    return (
+        <div>
+            <button onClick={() => setIsShow({
+                isShow: !isShow
+            })}>切换</button>
+            <ThemeContext.Provider value={{ theme, setTheme }}>
+                <List show={true}></List>
+            </ThemeContext.Provider>
+        </div>
+    )
+}
+
+function List({ show }) {
+    console.log(show, '打印')
+    if (show) {
+        const { theme, setTheme } = use(ThemeContext);
+        console.log(theme)
+        return (
+            <div>
+                <h1>你好:{theme}</h1>
+            </div>
+        )
+    }
+    return <h1>啥也灭有</h1>;
+}
+function Use() {
+    return (
+        <div>
+            <h1>Use</h1>
+            <hr />
+            <hr />
+            <hr />
+            <ThemeBox ></ThemeBox>
+        </div>
+    )
+}
+
+export default Use;

+ 33 - 0
21.react/高阶/project3/src/components/UseLearnImperativeHandle.jsx

@@ -0,0 +1,33 @@
+import { useRef, useImperativeHandle } from 'react'
+function Child({ ref }) {
+    // forwardRef
+    const inputRef = useRef(null);
+    useImperativeHandle(ref, () => {
+        console.log('执行useImperativeHandle', inputRef.current?.value)
+        return inputRef.current;
+    }, []);
+    return (
+        <div>
+            <h1>Child</h1>
+            <input type="text" ref={inputRef} />
+        </div>
+    )
+}
+
+function UseLearnImperativeHandle() {
+    const mainRef = useRef(null);
+    const handleClick = () => {
+        console.log('执行', mainRef)
+    }
+    return (
+        <div>
+            <Child ref={mainRef}></Child>
+            <hr />
+            <hr />
+            <h1>UseImperativeHandle</h1>
+            <button onClick={handleClick}>获取</button>
+        </div>
+    )
+}
+
+export default UseLearnImperativeHandle;

+ 81 - 0
21.react/高阶/project3/src/components/UseLearnOptimistic.jsx

@@ -0,0 +1,81 @@
+import { useState, useOptimistic, startTransition } from "react";
+// 网络请求
+const api = {
+    async delWay(ids) {
+        await new Promise((resolve, reject) => {
+            setTimeout(() => {
+                Math.random() > 0.1 ? resolve() : reject()
+            }, 3000)
+        })
+    }
+}
+// const [optimisticState, setOptimistic] = useOptimistic(value, reducer?);
+function UseLearnOptimistic() {
+    // 批量删除
+    const [todos, setTodos] = useState([
+        {
+            id: 1, text: '吃饭', completed: false
+        },
+        {
+            id: 2, text: '睡觉', completed: true
+        },
+        {
+            id: 3, text: '打豆豆', completed: false
+        }
+    ])
+    const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, (state, idList) => state.filter(item => !idList.includes(item.id)));
+    const [selectIds, setSelectIds] = useState(new Set());
+    // 选中项
+    const handleChange = (val) => {
+        setSelectIds(prev => {
+            const next = new Set(prev);
+            next.has(val) ? next.delete(val) : next.add(val)
+            return next;
+        })
+        setTodos((prev) => {
+            return prev.map(item => {
+                if (item.id == val) {
+                    item.completed = !item.completed
+                }
+                return item;
+            })
+        })
+
+    }
+    // 删除事件
+    async function handleDelete() {
+        // 获取选中的id
+        let ids = [...selectIds];
+        optimisticTodos.map(item => {
+            if (item.completed) {
+                ids.push(item.id)
+            }
+        })
+        // 乐观更新:立即从UI页面中 完成操作 不等后端实现
+        setOptimisticTodos(ids)
+        // 发起后端请求 删除选中的id
+        try {
+            const result = await api.delWay(ids);
+            setTodos(prev => prev.filter(item => !ids.includes(item.id)))
+        } catch (err) {
+            throw new Error(err)
+        }
+    }
+    // 渲染
+    return (
+        <div>
+            <h1>乐观请求</h1>
+            {
+                optimisticTodos.map((item) => (
+                    <div key={item.id}>
+                        <input type="checkbox" checked={item.completed} onChange={() => handleChange(item.id)} />
+                        <span>{item.text}</span>
+                    </div>
+                ))
+            }
+            <button onClick={() => startTransition(handleDelete)}>删除选中项</button>
+        </div>
+    )
+}
+
+export default UseLearnOptimistic;