This commit is contained in:
Resrate 2025-03-03 17:27:55 +08:00
commit 7631aa1cb7
20 changed files with 3804 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# vue2048-style
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

15
index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" src="/src/main.ts"></script>
<title>Vite App</title>
<style>
body{margin: 0px 0px;}
</style>
</head>
<body id="app">
</body>
</html>

3175
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "vue2048-style",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@vueuse/core": "^12.7.0",
"gsap": "^3.12.7",
"vue": "^3.5.13"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2",
"typescript": "~5.7.3",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.0",
"vue-tsc": "^2.2.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

78
src/App.vue Normal file
View File

@ -0,0 +1,78 @@
<style scoped>
#table {
background-color: #beb1a0;
width: 350px;
height: 350px;
position: relative;
margin: 10px auto;
padding: 0px 0px;
border-radius: 10px;
border: #857c71 solid 2px;
}
#first_line,
#last_line {
display: flex;
width: 350px;
justify-content: space-between;
margin: 0px auto;
}
#first_line div,
#last_line div{
margin-top: 8px;
}
#score {
background-color: #beb1a0;
width: 170px;
border-radius: 3px;
border: #857c71 solid 1px;
}
h1{
margin: 0px;
font-weight: bolder;
font-size: 300%;
}
button{
background-color: #beb1a0;
border-radius: 2px;
border: #857c71 solid 1px;
color: #736d64;
}
.fonts{
color: #736d64;
}
.font_white{
color: whitesmoke;
}
#num{
font-size: smaller;
}
</style>
<template>
<!-- <check :show="show" :result="result"></check> -->
<div id="first_line" class="fonts">
<h1>2048</h1>
<div id="score" class="font_white">&nbsp;</div>
</div>
<div id="last_line" class="fonts">
<div id="num">合并数字达到2048</div>
<button class="font_white">重新开始游戏</button>
</div>
<div id="table">
<buttom></buttom>
<move></move>
</div>
</template>
<script lang="ts" setup>
import buttom from './components/buttom.vue';
import move from './components/move.vue';
import check from './components/check.vue';
import { ref } from 'vue';
const show=ref(false);
const result=ref("")
const chenge=(shows:boolean,results:"胜利"|"失败")=>{
show.value=shows;
result.value=results;
}
</script>

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

3
src/assets/main.css Normal file
View File

@ -0,0 +1,3 @@
body{
background-color: #fcf6e3;
}

61
src/components/buttom.vue Normal file
View File

@ -0,0 +1,61 @@
<style scoped>
.buttom {
background-color: #cec0b3;
position: absolute;
padding: 0px 0px;
border: #857c71 solid 2px;
border-radius: 7px;
z-index: 0;
scale: 0;
width: 75px;
height: 75px;
}
</style>
<template>
<transition-group :css="false" name="div" @enter="enter" tag="div" >
<div
v-for="obj in arr"
key="obj"
class="buttom"
v-show="obj.show"
:style="{ top: `${obj.y * 85 + 10}px`, left: `${obj.x * 85 + 10}px` }"
></div>
</transition-group>
</template>
<script lang="ts" setup>
import { useEventListener } from '@vueuse/core';
import gsap from 'gsap';
import { onMounted, ref } from 'vue';
import timmer from '../ts/my_ts_pack';
type GSAPTweenTarget = string | Element | Element[] | object;
const arr = ref(
Array.from(Array(16), (_, index) => ({
show: false,
x: index % 4,
y: Math.floor(index / 4),
}))
);
const enter = (vDom: gsap.TweenTarget, done: () => void) => {
gsap.to(vDom, {
scale:1,
duration:1.2,
ease: "back.out(1.5)",
onComplete:done
});
};
onMounted(() => {
useEventListener(window, 'load', async () => {
for (let i = 0; i <= 7; i++) {
await timmer(120);
for (let j = 0; j <= i; j++) {
const y = j;
const x = i - j;
if (arr.value[y * 4 + x]!=undefined)
arr.value[y * 4 + x].show = true;
}
}
});
});
</script>

112
src/components/check.vue Normal file
View File

@ -0,0 +1,112 @@
<style scoped>
* {
position: absolute;
text-align: center;
width: 100%;
height: auto;
z-index: 30;
}
#white {
width: 100%;
height: 100%;
background-color: rgba(125, 125, 125, 0.6);
}
#win {
font-family: '楷体';
font-size: 500%;
top:10%;
}
#time{
top: 50%;
}
#tick{
top:53%
}
</style>
<template>
<Transition @enter="obj.white.animate">
<div id="white" v-show="obj.white.show">&nbsp;</div>
</Transition>
<Transition @enter="obj.win.animate">
<div id="win" v-show="obj.white.show">
<h1>{{ props.result }}</h1>
</div>
</Transition>
<Transition @enter="obj.time.animate">
<div id="time" v-show="obj.white.show">时间(还未做),分数(还未做)</div>
</Transition>
<Transition @enter="obj.tick.animate">
<div id="tick" v-show="obj.white.show">单击任意一处即可继续游戏</div>
</Transition>
</template>
<script setup lang="ts">
import { reactive, ref, Transition, watch } from 'vue';
import timmer from '../ts/my_ts_pack';
import gsap from 'gsap';
type GSAPTweenTarget = string | Element | Element[] | object;
const props = defineProps<{
result: string;
show: boolean;
}>();
const obj = reactive({
white: {
show: ref(false),
animate: (vDom: GSAPTweenTarget, done: () => void) => {
gsap.from(vDom, {
opacity: 0,
duration: 1,
onComplete: done,
});
},
},
win: {
show: ref(false),
animate: (vDom: GSAPTweenTarget, done: () => void) => {
gsap.from(vDom, {
opacity: 0,
duration: 2,
y: -1000,
ease: "bounce.out",
onComplete: done,
});
},
},
time: {
show: ref(false),
animate: (vDom: GSAPTweenTarget, done: () => void) => {
gsap.from(vDom, {
opacity: 0,
y: window.innerHeight,
duration: 1,
onComplete: done,
});
},
},
tick: {
show: ref(false),
animate: (vDom: GSAPTweenTarget, done: () => void) => {
gsap.from(vDom, {
opacity: 0,
duration: 4,
ease:"power4.in",
onComplete: done,
});
},
},
});
watch(
() => props.show,
async () => {
obj.white.show = props.show;
obj.win.show = props.show;
await timmer(1000);
obj.time.show = props.show;
await timmer(1000);
obj.tick.show = props.show;
}
);
</script>

187
src/components/move.vue Normal file
View File

@ -0,0 +1,187 @@
<style scoped>
.move {
z-index: 10;
color: #736d64;
font-weight: bolder;
font-size: large;
text-align: center;
line-height: 74px;
background-color: #efe0c9;
width: 74px;
height: 74px;
position: absolute;
margin: 0px 0px;
padding: 0px 0px;
border-radius: 7px;
}
</style>
<template>
<div
class="move"
ref="blockes"
v-for="dom in block"
v-show="dom.value"
:style="{ zIndex: dom.merge === true ? 10 : 0 }"
>
{{ dom.value }}
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import gsap from 'gsap';
import timmer from '../ts/my_ts_pack';
import { useEventListener } from '@vueuse/core';
const blockes = ref<HTMLElement[]>([]);
class Block {
constructor(
public index: number,
public from = { x: -1, y: -1 },
public to = { x: -1, y: -1 },
public value = 0,
public digitalValue = 0,
public endValue = 0,
public merge = false as boolean | Block,
public create = false
) {}
get positionForm() {
return {
x: this.from.x * 85 + 12,
y: this.from.y * 85 + 12,
};
}
get positionTo() {
return {
x: this.to.x * 85 + 12,
y: this.to.y * 85 + 12,
duration: 0.5, //
};
}
digitalMove(deltaY: number, deltaX: number) {
const target = table.value[this.to.y + deltaY]?.[this.to.x + deltaX];
if (target === undefined) return;
if (target === null) {
this.to.x += deltaX;
this.to.y += deltaY;
return;
}
if (target.digitalValue == this.digitalValue) {
if (this.merge == false) {
this.merge = target;
this.digitalValue = this.endValue = 0;
target.merge = true;
target.digitalValue *= 2;
target.endValue *= 2;
}
return;
}
}
allAnimation() {
const self = blockes.value[this.index];
const timeLine=gsap.timeline()
timeLine.add(gsap.set(self, { scale: 1 }));
timeLine.add(gsap.fromTo(self, this.positionForm, this.positionTo), '<');
if (this.merge == true) {
this.value *= 2;
const self = blockes.value[this.index];
timeLine.add(gsap.to(self, { scale: 1.1, duration: 0.2 }));
timeLine.add(gsap.to(self, { scale: 1, duration: 0.2 }));
}
if (typeof this.merge == 'object') {
this.merge = false;
}
if (this.create) {
this.create = false;
gsap.from(self, { scale: 0, duration: 0.3 ,delay:0.1});
}
}
syncing(target: 'from&to' | 'merge&xy') {
if (target == 'from&to') {
this.from.x = this.to.x;
this.from.y = this.to.y;
}
if (target == 'merge&xy' && typeof this.merge == 'object') {
this.to.x = this.merge.to.x;
this.to.y = this.merge.to.y;
}
}
digitalCreate() {
while (true) {
const newX = Math.floor(Math.random() * 4);
const newY = Math.floor(Math.random() * 4);
if (table.value[newY][newX] === null) {
this.from.x = this.to.x = newX;
this.from.y = this.to.y = newY;
this.digitalValue = this.value = this.endValue = 2;
break;
}
}
}
}
const block = reactive(
Array.from({ length: 16 }, (_, index) => new Block(index))
);
const table = computed<(Block | null)[][]>(() => {
const result = Array.from({ length: 4 }, () =>
Array.from({ length: 4 }, () => null)
) as (null | Block)[][];
block.forEach((element) => {
if (element.value && typeof element.merge == 'boolean')
result[element.to.y][element.to.x] = element;
});
return result;
});
const controller = async (e: KeyboardEvent) => {
let createFlag = true;
switch (e.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
block.forEach((element) => {
element.syncing('from&to');
const result = element.endValue;
element.value = element.digitalValue = result;
element.create = element.merge = false;
});
}
for (let i = 1; i <= 3; i++) {
switch (e.key) {
case 'ArrowUp':
block.forEach((element) => {
if (element.value) element.digitalMove(-1, 0);
});
break;
case 'ArrowDown':
block.forEach((element) => {
if (element.value) element.digitalMove(1, 0);
});
break;
case 'ArrowLeft':
block.forEach((element) => {
if (element.value) element.digitalMove(0, -1);
});
break;
case 'ArrowRight':
block.forEach((element) => {
if (element.value) element.digitalMove(0, 1);
});
break;
default:
createFlag = false;
}
}
block.forEach((element) => {
if (!element.value && createFlag) {
element.digitalCreate();
createFlag = false;
element.create=true;
}
element.syncing('merge&xy');
element.allAnimation();
element.create=false;
});
};
onMounted(() => {
useEventListener(window, 'keydown', controller);
});
</script>

6
src/main.ts Normal file
View File

@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app');

11
src/ts/my_ts_pack.ts Normal file
View File

@ -0,0 +1,11 @@
export default (time: number, method?: () => void) =>
new Promise((r, j) => {
setTimeout(() => {
try {
if (method) method();
r(true);
} catch (e) {
j(e);
}
}, time);
});

11
tsconfig.app.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

18
vite.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})