更新状态中的数组

数组在 JavaScript 中是可变的,但在将它们存储在状态中时,你应该将它们视为不可变的。就像对象一样,当你想更新存储在状态中的数组时,你需要创建一个新数组(或复制现有数组),然后设置状态以使用新数组。

¥Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array.

你将学习到

  • 如何在 React 状态下添加、删除或更改数组中的条目

    ¥How to add, remove, or change items in an array in React state

  • 如何更新数组内的对象

    ¥How to update an object inside of an array

  • 如何使用 Immer 减少数组复制的重复性

    ¥How to make array copying less repetitive with Immer

更新数组而不改变

¥Updating arrays without mutation

在 JavaScript 中,数组只是另一种对象。就像对象,你应该将 React 状态下的数组视为只读。这意味着你不应该重新分配像 arr[0] = 'bird' 这样的数组内的项目,也不应该使用改变数组的方法,例如 push()pop()

¥In JavaScript, arrays are just another kind of object. Like with objects, you should treat arrays in React state as read-only. This means that you shouldn’t reassign items inside an array like arr[0] = 'bird', and you also shouldn’t use methods that mutate the array, such as push() and pop().

而是,每次你想更新一个数组时,你都需要将一个新数组传递给你的状态设置函数。为此,你可以通过调用其非修改方法(如 filter()map())从你所在状态的原始数组创建一个新数组。然后你可以将你的状态设置为生成的新数组。

¥Instead, every time you want to update an array, you’ll want to pass a new array to your state setting function. To do that, you can create a new array from the original array in your state by calling its non-mutating methods like filter() and map(). Then you can set your state to the resulting new array.

这里有一个常见数组操作的参考表。在处理 React 状态中的数组时,你需要避免使用左栏中的方法,而更优先右栏中的方法:

¥Here is a reference table of common array operations. When dealing with arrays inside React state, you will need to avoid the methods in the left column, and instead prefer the methods in the right column:

避免(改变数组)更喜欢(返回新数组)
添加push, unshiftconcat[...arr] 展开语法 (示例)
删除pop, shift, splicefilter, slice (示例)
替换splicearr[i] = ... 赋值map(示例)
排序reverse, sort首先复制数组 (示例)

或者,你可以使用 使用 Immer,它允许你使用两列中的方法。

¥Alternatively, you can use Immer which lets you use methods from both columns.

易犯错误

不幸的是,slicesplice 的命名相似但差别很大:

¥Unfortunately, slice and splice are named similarly but are very different:

  • slice 允许你复制一个数组或其中的一部分。

    ¥slice lets you copy an array or a part of it.

  • splice 改变数组(插入或删除项目)。

    ¥splice mutates the array (to insert or delete items).

在 React 中,你将更频繁地使用 slice(没有 p!),因为你不想改变状态中的对象或数组。更新对象 解释了什么是突变以及为什么不建议将其用于状态。

¥In React, you will be using slice (no p!) a lot more often because you don’t want to mutate objects or arrays in state. Updating Objects explains what mutation is and why it’s not recommended for state.

添加到数组

¥Adding to an array

push() 会改变一个你不想改变的数组:

¥push() will mutate an array, which you don’t want:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

而是,创建一个新数组,其中包含现有项目和末尾的新项目。有多种方法可以做到这一点,但最简单的方法是使用 ... 数组展开 语法:

¥Instead, create a new array which contains the existing items and a new item at the end. There are multiple ways to do this, but the easiest one is to use the ... array spread syntax:

setArtists( // Replace the state
[ // with a new array
...artists, // that contains all the old items
{ id: nextId++, name: name } // and one new item at the end
]
);

现在它工作正常:

¥Now it works correctly:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

数组扩展语法还允许你通过将条目放在原始 ...artists 之前来预先添加项目:

¥The array spread syntax also lets you prepend an item by placing it before the original ...artists:

setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);

这样,展开可以通过添加到数组的末尾来完成 push() 的工作,也可以通过添加到数组的开头来完成 unshift() 的工作。在上面的沙盒中试试吧!

¥In this way, spread can do the job of both push() by adding to the end of an array and unshift() by adding to the beginning of an array. Try it in the sandbox above!

从数组中删除

¥Removing from an array

从数组中删除项目的最简单方法是将其过滤掉。换句话说,你将生成一个不包含该条目的新数组。为此,请使用 filter 方法,例如:

¥The easiest way to remove an item from an array is to filter it out. In other words, you will produce a new array that will not contain that item. To do this, use the filter method, for example:

import { useState } from 'react';

let initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [artists, setArtists] = useState(
    initialArtists
  );

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

单击 “删除” 按钮几次,然后查看其单击处理程序。

¥Click the “Delete” button a few times, and look at its click handler.

setArtists(
artists.filter(a => a.id !== artist.id)
);

这里,artists.filter(a => a.id !== artist.id) 的意思是“创建一个由 ID 与 artist.id 不同的 artists 组成的数组”。换句话说,每个艺术家的 “删除” 按钮将从数组中过滤出该艺术家,然后请求使用结果数组重新渲染。请注意,filter 不会修改原始数组。

¥Here, artists.filter(a => a.id !== artist.id) means “create an array that consists of those artists whose IDs are different from artist.id”. In other words, each artist’s “Delete” button will filter that artist out of the array, and then request a re-render with the resulting array. Note that filter does not modify the original array.

转换数组

¥Transforming an array

如果要更改数组的部分或全部项目,可以使用 map() 创建新数组。你将传递给 map 的函数可以根据其数据或索引(或两者)决定如何处理每个条目。

¥If you want to change some or all items of the array, you can use map() to create a new array. The function you will pass to map can decide what to do with each item, based on its data or its index (or both).

在此示例中,数组包含两个圆和一个方块的坐标。当你按下按钮时,它只会将圆圈向下移动 50 像素。它通过使用 map() 生成一个新的数据数组来做到这一点:

¥In this example, an array holds coordinates of two circles and a square. When you press the button, it moves only the circles down by 50 pixels. It does this by producing a new array of data using map():

import { useState } from 'react';

let initialShapes = [
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
];

export default function ShapeEditor() {
  const [shapes, setShapes] = useState(
    initialShapes
  );

  function handleClick() {
    const nextShapes = shapes.map(shape => {
      if (shape.type === 'square') {
        // No change
        return shape;
      } else {
        // Return a new circle 50px below
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Re-render with the new array
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Move circles down!
      </button>
      {shapes.map(shape => (
        <div
          key={shape.id}
          style={{
          background: 'purple',
          position: 'absolute',
          left: shape.x,
          top: shape.y,
          borderRadius:
            shape.type === 'circle'
              ? '50%' : '',
          width: 20,
          height: 20,
        }} />
      ))}
    </>
  );
}

替换数组中的条目

¥Replacing items in an array

想要替换数组中的一个或多个条目是特别常见的。像 arr[0] = 'bird' 这样的赋值正在改变原始数组,因此你也需要为此使用 map

¥It is particularly common to want to replace one or more items in an array. Assignments like arr[0] = 'bird' are mutating the original array, so instead you’ll want to use map for this as well.

要替换项目,请使用 map 创建一个新数组。在 map 调用中,你将收到项目索引作为第二个参数。用它来决定是否返回原始条目(第一个参数)或其他东西:

¥To replace an item, create a new array with map. Inside your map call, you will receive the item index as the second argument. Use it to decide whether to return the original item (the first argument) or something else:

import { useState } from 'react';

let initialCounters = [
  0, 0, 0
];

export default function CounterList() {
  const [counters, setCounters] = useState(
    initialCounters
  );

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // Increment the clicked counter
        return c + 1;
      } else {
        // The rest haven't changed
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

插入到数组

¥Inserting into an array

有时,你可能希望在既不是开头也不是结尾的特定位置插入一个条目。为此,你可以将 ... 数组展开语法与 slice() 方法结合使用。slice() 方法可让你剪切数组的 “切片”。要插入一个条目,你将创建一个数组,该数组展开插入点之前的切片,然后是新条目,最后是原始数组的其余部分。

¥Sometimes, you may want to insert an item at a particular position that’s neither at the beginning nor at the end. To do this, you can use the ... array spread syntax together with the slice() method. The slice() method lets you cut a “slice” of the array. To insert an item, you will create an array that spreads the slice before the insertion point, then the new item, and then the rest of the original array.

在此示例中,“插入”按钮始终在索引 1 处插入:

¥In this example, the Insert button always inserts at the index 1:

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Could be any index
    const nextArtists = [
      // Items before the insertion point:
      ...artists.slice(0, insertAt),
      // New item:
      { id: nextId++, name: name },
      // Items after the insertion point:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insert
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

对数组进行其他更改

¥Making other changes to an array

有些事情你不能单独使用扩展语法和非修改方法,如 map()filter()。例如,你可能想要对数组进行反转或排序。JavaScript reverse()sort() 方法正在改变原始数组,因此你不能直接使用它们。

¥There are some things you can’t do with the spread syntax and non-mutating methods like map() and filter() alone. For example, you may want to reverse or sort an array. The JavaScript reverse() and sort() methods are mutating the original array, so you can’t use them directly.

但是,你可以先复制数组,然后再对其进行更改。

¥However, you can copy the array first, and then make changes to it.

例如:

¥For example:

import { useState } from 'react';

const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        Reverse
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

在这里,你首先使用 [...list] 扩展语法创建原始数组的副本。现在你有了一个副本,你可以使用 nextList.reverse()nextList.sort() 等修改方法,甚至可以使用 nextList[0] = "something" 分配单个条目。

¥Here, you use the [...list] spread syntax to create a copy of the original array first. Now that you have a copy, you can use mutating methods like nextList.reverse() or nextList.sort(), or even assign individual items with nextList[0] = "something".

但是,即使复制数组,也无法直接改变其中的现有项。这是因为复制是浅层的 - 新数组将包含与原始数组相同的项目。因此,如果你修改复制数组中的对象,就会改变现有状态。例如,像这样的代码就是一个问题。

¥However, even if you copy an array, you can’t mutate existing items inside of it directly. This is because copying is shallow—the new array will contain the same items as the original one. So if you modify an object inside the copied array, you are mutating the existing state. For example, code like this is a problem.

const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);

虽然 nextListlist 是两个不同的数组,但 nextList[0]list[0] 指向同一个对象。因此,通过更改 nextList[0].seen,你也会更改 list[0].seen。这是状态突变,你应该避免!你可以通过与 更新嵌套的 JavaScript 对象 类似的方式解决此问题 - 通过复制你想要更改的单个条目而不是改变它们。就是这样。

¥Although nextList and list are two different arrays, nextList[0] and list[0] point to the same object. So by changing nextList[0].seen, you are also changing list[0].seen. This is a state mutation, which you should avoid! You can solve this issue in a similar way to updating nested JavaScript objects—by copying individual items you want to change instead of mutating them. Here’s how.

更新数组中的对象

¥Updating objects inside arrays

对象并不是真正位于数组 “里面”。它们在代码中可能看起来是 “里面”,但数组中的每个对象都是一个单独的值,数组 “指向” 为该值。这就是为什么在更改像 list[0] 这样的嵌套字段时需要小心。另一个人的作品列表可能指向数组的同一个元素!

¥Objects are not really located “inside” arrays. They might appear to be “inside” in code, but each object in an array is a separate value, to which the array “points”. This is why you need to be careful when changing nested fields like list[0]. Another person’s artwork list may point to the same element of the array!

更新嵌套状态时,你需要从要更新的点开始创建副本,一直到顶层。让我们看看这是如何工作的。

¥When updating nested state, you need to create copies from the point where you want to update, and all the way up to the top level. Let’s see how this works.

在此示例中,两个单独的艺术品列表具有相同的初始状态。它们应该是隔离的,但由于突变,它们的状态被意外共享,选中一个列表中的框会影响另一个列表:

¥In this example, two separate artwork lists have the same initial state. They are supposed to be isolated, but because of a mutation, their state is accidentally shared, and checking a box in one list affects the other list:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

问题出在这样的代码中:

¥The problem is in code like this:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);

虽然 myNextList 数组本身是新的,但条目本身与原始 myList 数组中的相同。因此,更改 artwork.seen 会更改原始艺术品条目。该艺术品条目也在 yourList 中,这导致了错误。像这样的错误可能很难考虑,但值得庆幸的是,如果你避免改变状态,它们就会消失。

¥Although the myNextList array itself is new, the items themselves are the same as in the original myList array. So changing artwork.seen changes the original artwork item. That artwork item is also in yourList, which causes the bug. Bugs like this can be difficult to think about, but thankfully they disappear if you avoid mutating state.

你可以使用 map 将旧条目替换为其更新版本而不会发生突变。

¥You can use map to substitute an old item with its updated version without mutation.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));

这里,... 是用于 创建对象的副本 的对象展开语法。

¥Here, ... is the object spread syntax used to create a copy of an object.

使用这种方法,现有状态项都不会发生变化,并且错误已修复:

¥With this approach, none of the existing state items are being mutated, and the bug is fixed:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

一般来说,你应该只改变刚刚创建的对象。如果你要插入新的艺术品,则可以对其进行更改,但如果你正在处理已经处于状态的东西,则需要制作副本。

¥In general, you should only mutate objects that you have just created. If you were inserting a new artwork, you could mutate it, but if you’re dealing with something that’s already in state, you need to make a copy.

使用 Immer 编写简洁的更新逻辑

¥Write concise update logic with Immer

不改变地更新嵌套数组可能会有点重复。就像对象一样

¥Updating nested arrays without mutation can get a little bit repetitive. Just as with objects:

  • 通常,你不需要更新超过几个层级的状态。如果你的状态对象非常深,你可能需要 以不同的方式重组它们 以便它们变平。

    ¥Generally, you shouldn’t need to update state more than a couple of levels deep. If your state objects are very deep, you might want to restructure them differently so that they are flat.

  • 如果你不想更改你的状态结构,你可能更喜欢使用 Immer,它允许你使用方便但可变的语法进行编写,并负责为你生成副本。

    ¥If you don’t want to change your state structure, you might prefer to use Immer, which lets you write using the convenient but mutating syntax and takes care of producing the copies for you.

这是用 Immer 重写的艺术遗愿清单示例:

¥Here is the Art Bucket List example rewritten with Immer:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

请注意,使用 Immer,像 artwork.seen = nextSeen 这样的突变现在是可以的:

¥Note how with Immer, mutation like artwork.seen = nextSeen is now okay:

updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});

这是因为你没有改变原始状态,而是改变了 Immer 提供的特殊 draft 对象。同样,你可以将 push()pop() 等修改方法应用于 draft 的内容。

¥This is because you’re not mutating the original state, but you’re mutating a special draft object provided by Immer. Similarly, you can apply mutating methods like push() and pop() to the content of the draft.

在幕后,Immer 总是根据你对 draft 所做的更改从头开始构建下一个状态。这使你的事件处理程序非常简洁,而不会改变状态。

¥Behind the scenes, Immer always constructs the next state from scratch according to the changes that you’ve done to the draft. This keeps your event handlers very concise without ever mutating state.

回顾

  • 你可以将数组放入状态,但不能更改它们。

    ¥You can put arrays into state, but you can’t change them.

  • 与其改变数组,不如创建它的新版本,并更新它的状态。

    ¥Instead of mutating an array, create a new version of it, and update the state to it.

  • 你可以使用 [...arr, newItem] 数组展开语法来创建包含新项的数组。

    ¥You can use the [...arr, newItem] array spread syntax to create arrays with new items.

  • 你可以使用 filter()map() 创建具有过滤或转换项目的新数组。

    ¥You can use filter() and map() to create new arrays with filtered or transformed items.

  • 你可以使用 Immer 来保持代码简洁。

    ¥You can use Immer to keep your code concise.

挑战 1 / 4:
更新购物车中的商品

¥Update an item in the shopping cart

填写 handleIncreaseClick 逻辑,使按 ”*” 增加相应的数字:

¥Fill in the handleIncreaseClick logic so that pressing ”+” increases the corresponding number:

import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Cheese',
  count: 5,
}, {
  id: 2,
  name: 'Spaghetti',
  count: 2,
}];

export default function ShoppingCart() {
  const [
    products,
    setProducts
  ] = useState(initialProducts)

  function handleIncreaseClick(productId) {

  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {' '}
          (<b>{product.count}</b>)
          <button onClick={() => {
            handleIncreaseClick(product.id);
          }}>
            +
          </button>
        </li>
      ))}
    </ul>
  );
}


React 中文网 - 粤ICP备13048890号