title: 俄罗斯方块
description: 俄罗斯方块的背景组件，你甚至可以点击一个方块来消除它。
---

```vue
<template>
  <ClientOnly>
    <Tetris
      class="h-[500px] w-full [mask-image:radial-gradient(450px_circle_at_center,#00C16A,transparent)]"
      :base="15"
      square-color="#00C16A"
    />
  </ClientOnly>
</template>

<script lang="ts" setup></script>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

::alert{type="info"}
**注意:** This component requires `theme-colors` as a dependency.
::

## API

| Prop 名称      | 类型     | 默认值 | 描述                                           |
| -------------- | -------- | ------ | ---------------------------------------------- |
| `class`        | `string` | `""`   | Additional class names to style the component. |
| `base`         | `number` | `10`   | How many blocks do you have in a row.          |
| `square-color` | `string` | `""`   | Square color.                                  |

## 通过 CLI 安装

```vue
<InstallationCli component-id="tetris" />
```

## 手动安装

::steps{level=4}

#### Install the dependencies

::code-group

    ```bash [npm]
    npm install theme-colors
    ```

    ```bash [pnpm]
    pnpm install theme-colors
    ```

    ```bash [bun]
    bun add theme-colors
    ```

    ```bash [yarn]
    yarn add theme-colors
    ```

::

复制并粘贴以下代码：

```vue
<template>
  <Transition
    appear
    name="fade"
  >
    <div
      :style="{
        '--cell-size': `${width / cols}px`,
        '--grid-rows': rows - 1,
      }"
      :class="cn('relative w-full', props.class)"
    >
      <div
        ref="el"
        class="absolute inset-0 grid justify-center -space-y-px"
        :style="{ gridTemplateRows: `repeat(var(--grid-rows), var(--cell-size))` }"
      >
        <div
          v-for="(row, rowIndex) in grid"
          :key="rowIndex"
          class="grid flex-1 grid-flow-col -space-x-px"
          :style="{ gridTemplateColumns: `repeat(${cols}, var(--cell-size))` }"
        >
          <div
            v-for="(cell, cellIndex) in row"
            :key="cellIndex"
            :style="{
              '--border-light': theme[100],
              '--border-dark': theme[900],
              '--square-light': theme[500],
              '--square-hover-light': theme[400],
              '--square-dark': theme[700],
              '--square-hover-dark': theme[600],
            }"
            class="relative border border-[color:var(--border-light)] dark:border-[color:var(--border-dark)]"
          >
            <div
              class="absolute inset-0 bg-[color:var(--square-light)] opacity-0 transition-opacity duration-1000 will-change-[opacity] hover:bg-[color:var(--square-hover-light)] dark:bg-[color:var(--square-dark)] dark:hover:bg-[color:var(--square-hover-dark)]"
              :class="[cell && 'cursor-pointer opacity-60']"
              @click="cell && removeCell(rowIndex, cellIndex)"
            />
          </div>
        </div>
      </div>
    </div>
  </Transition>
</template>

<script setup lang="ts">
import { useElementSize } from "@vueuse/core";
import { cn } from "@/lib/utils";
import { getColors } from "theme-colors";
import { ref, onMounted, onUnmounted, watch } from "vue";

interface Props {
  class?: string;
  squareColor: string;
  base?: number;
}

const props = withDefaults(defineProps<Props>(), {
  base: 10,
});

const theme = getColors(props.squareColor);

const el = ref(null);
const grid = ref<(boolean | null)[][]>([]);
const rows = ref(0);
const cols = ref(0);

const { width, height } = useElementSize(el);

function createGrid() {
  grid.value = [];

  for (let i = 0; i < rows.value; i++) {
    grid.value.push(new Array(cols.value).fill(null));
  }
}

function createNewCell() {
  const x = Math.floor(Math.random() * cols.value);

  grid.value[0][x] = true;
}

function moveCellsDown() {
  for (let row = rows.value - 1; row >= 0; row--) {
    for (let col = 0; col < cols.value; col++) {
      const cell = grid.value[row][col];
      const nextCell = Array.isArray(grid.value[row + 1]) ? grid.value[row + 1][col] : cell;
      if (cell !== null && nextCell === null) {
        grid.value[row + 1][col] = grid.value[row][col];
        grid.value[row][col] = null;
      }
    }
  }

  setTimeout(() => {
    const isFilled = grid.value[rows.value - 1].every((cell) => cell !== null);
    if (Array.isArray(grid.value[rows.value]) && isFilled) {
      for (let col = 0; col < cols.value; col++) {
        grid.value[rows.value][col] = null;
      }
    }
  }, 500);
}

function clearColumn() {
  const isFilled = grid.value[rows.value - 1].every((cell) => cell === true);
  if (!isFilled) return;

  for (let col = 0; col < cols.value; col++) {
    grid.value[rows.value - 1][col] = null;
  }
}

function removeCell(row: number, col: number) {
  grid.value[row][col] = null;
}

function calcGrid() {
  const cell = width.value / props.base;

  rows.value = Math.floor(height.value / cell);
  cols.value = Math.floor(width.value / cell);

  createGrid();
}

watch(width, calcGrid);

// eslint-disable-next-line no-undef
let intervalId: NodeJS.Timeout | undefined;
// eslint-disable-next-line no-undef
let timeoutId: NodeJS.Timeout | undefined;

onMounted(() => {
  timeoutId = setTimeout(calcGrid, 50);

  intervalId = setInterval(() => {
    clearColumn();
    moveCellsDown();
    createNewCell();
  }, 1000);
});

onUnmounted(() => {
  clearInterval(intervalId);
  clearTimeout(timeoutId);
});
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

```

::

## 感谢

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired and ported from [Nuxt UI Home](https://ui2.nuxt.com/).