refactor: 主题初始化、资源缓存、UI交互等重构
|
@ -26,5 +26,6 @@ module.exports = {
|
|||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/ban-ts-comment': 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
},
|
||||
};
|
||||
|
|
177
index.html
|
@ -19,18 +19,185 @@
|
|||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
transition: background-color 0.3s 0.4s;
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
a {
|
||||
color: currentColor;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
#root {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
#loading {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
overflow: visible;
|
||||
}
|
||||
@keyframes move {
|
||||
0% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
12.5% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
25% {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
37.5% {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
62.5% {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
75% {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
87.5% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: translateY(30px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
}
|
||||
.loadingBlock {
|
||||
position: absolute;
|
||||
transition: 0.6s;
|
||||
border-radius: 12px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
animation: move 1s infinite ease-in-out;
|
||||
}
|
||||
.loadingBlockContainer {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
}
|
||||
#loading.error .loadingBlock,
|
||||
#loading.success .loadingBlock {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.loadingBlock1 {
|
||||
background-color: #8dac8588;
|
||||
}
|
||||
#loading.error .loadingBlock1 {
|
||||
transform: rotate(75deg) translateX(30px);
|
||||
animation: wave 1s infinite alternate;
|
||||
}
|
||||
#loading.success .loadingBlock1 {
|
||||
transform: rotate(75deg) translateX(200px) scale(4);
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
.loadingBlock2 {
|
||||
background-color: #8dac8566;
|
||||
animation-delay: 0.375s;
|
||||
}
|
||||
#loading.error .loadingBlock2 {
|
||||
transform: rotate(175deg) translateX(10px);
|
||||
}
|
||||
#loading.success .loadingBlock2 {
|
||||
transform: rotate(175deg) translateX(200px) scale(2);
|
||||
transition-delay: 0.05s;
|
||||
}
|
||||
.loadingBlock3 {
|
||||
background-color: #8dac8544;
|
||||
animation-delay: 0.75s;
|
||||
}
|
||||
#loading.error .loadingBlock3 {
|
||||
transform: rotate(225deg) translateX(20px);
|
||||
}
|
||||
#loading.success .loadingBlock3 {
|
||||
transform: rotate(225deg) translateX(200px) scale(3);
|
||||
}
|
||||
#loadingTips {
|
||||
font-size: 0.8em;
|
||||
line-height: 1.8;
|
||||
text-align: center;
|
||||
transition: 0.3s;
|
||||
}
|
||||
#loading.success #loadingTips {
|
||||
transform: translateY(300px);
|
||||
opacity: 0;
|
||||
}
|
||||
#backHomeTip {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root">
|
||||
<!--数据加载提示在react渲染之前做-->
|
||||
<div id="loading" class="loading">
|
||||
<div class="loadingBlockContainer">
|
||||
<div class="loadingBlock loadingBlock1"></div>
|
||||
<div class="loadingBlock loadingBlock2"></div>
|
||||
<div class="loadingBlock loadingBlock3"></div>
|
||||
</div>
|
||||
<div id="loadingTips">
|
||||
<span id="loadingText">加载中...</span><br />
|
||||
<span id="backHomeTip">
|
||||
稍后再试或
|
||||
<a href="/" style="color: #646cff"> 返回首页 </a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// vite没有global,手动声明
|
||||
var global = global || window;
|
||||
</script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script
|
||||
async
|
||||
src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"
|
||||
></script>
|
||||
<script async>
|
||||
// 如果您基于此项目二创,可以删除以下代码
|
||||
// 否则请标明原仓库地址
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true"
|
||||
class="octicon octicon-mark-github v-align-middle">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
Before Width: | Height: | Size: 788 B |
BIN
public/wxqrcode.png
Normal file
After Width: | Height: | Size: 62 KiB |
15
src/App.scss
|
@ -3,4 +3,19 @@
|
|||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
|
||||
>:not(.background) {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
}
|
||||
|
|
145
src/App.tsx
|
@ -1,76 +1,99 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import './App.scss';
|
||||
import {
|
||||
domRelatedOptForTheme,
|
||||
LAST_LEVEL_STORAGE_KEY,
|
||||
parsePathCustomThemeId,
|
||||
parsePathThemeName,
|
||||
LAST_SCORE_STORAGE_KEY,
|
||||
wrapThemeDefaultSounds,
|
||||
} from './utils';
|
||||
import { defaultTheme } from './themes/default';
|
||||
import { Theme } from './themes/interface';
|
||||
import { fishermanTheme } from './themes/fisherman';
|
||||
import { jinlunTheme } from './themes/jinlun';
|
||||
import { ikunTheme } from './themes/ikun';
|
||||
import { pddTheme } from './themes/pdd';
|
||||
import { owTheme } from './themes/ow';
|
||||
import Game from './Game';
|
||||
import { Loading } from './components/Loading';
|
||||
import Game from './components/Game';
|
||||
import { BeiAn } from './components/BeiAn';
|
||||
import { Title } from './components/Title';
|
||||
import { PersonalInfo } from './components/PersonalInfo';
|
||||
import { Info } from './components/Info';
|
||||
import { ThemeChanger } from './components/ThemeChanger';
|
||||
import { ConfigDialog } from './components/ConfigDialog';
|
||||
|
||||
// 内置主题
|
||||
const builtInThemes: Theme<any>[] = [
|
||||
defaultTheme,
|
||||
fishermanTheme,
|
||||
jinlunTheme,
|
||||
ikunTheme,
|
||||
pddTheme,
|
||||
owTheme,
|
||||
];
|
||||
// 从url初始化主题
|
||||
const themeFromPath = parsePathThemeName(location.href);
|
||||
const customThemeIdFromPath = parsePathCustomThemeId(location.href);
|
||||
const initTheme = customThemeIdFromPath
|
||||
? { title: '', icons: [], sounds: [], name: '' }
|
||||
: themeFromPath
|
||||
? builtInThemes.find((theme) => theme.name === themeFromPath) ??
|
||||
defaultTheme
|
||||
: defaultTheme;
|
||||
|
||||
// 读取缓存关卡数
|
||||
// 读取缓存关卡得分
|
||||
const initLevel = Number(localStorage.getItem(LAST_LEVEL_STORAGE_KEY) || '1');
|
||||
const initScore = Number(localStorage.getItem(LAST_SCORE_STORAGE_KEY) || '0');
|
||||
|
||||
const App: FC<{ theme: Theme<any> }> = ({ theme: initTheme }) => {
|
||||
console.log('initTheme', initTheme);
|
||||
// console.log(JSON.stringify(theme));
|
||||
|
||||
const App: FC = () => {
|
||||
console.log('???');
|
||||
const [theme, setTheme] = useState<Theme<any>>(initTheme);
|
||||
const [loading, setLoading] = useState<boolean>(!!customThemeIdFromPath);
|
||||
const [error, setError] = useState<string>('');
|
||||
if (customThemeIdFromPath) {
|
||||
// debugger
|
||||
// 自定义主题
|
||||
/*Bmob.Query('config')
|
||||
.get(customThemeIdFromPath)
|
||||
.then((res) => {
|
||||
// @ts-ignore
|
||||
const { content } = res;
|
||||
try {
|
||||
const customTheme = JSON.parse(content);
|
||||
setTheme(customTheme);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setError('主题配置解析失败');
|
||||
}
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
setError(error);
|
||||
});*/
|
||||
}
|
||||
const [diyDialogShow, setDiyDialogShow] = useState<boolean>(false);
|
||||
|
||||
return loading ? (
|
||||
<Loading error={error} />
|
||||
) : (
|
||||
const changeTheme = (theme: Theme<any>) => {
|
||||
wrapThemeDefaultSounds(theme);
|
||||
domRelatedOptForTheme(theme);
|
||||
setTheme({ ...theme });
|
||||
};
|
||||
|
||||
const previewTheme = (_theme: Theme<any>) => {
|
||||
const theme = JSON.parse(JSON.stringify(_theme));
|
||||
wrapThemeDefaultSounds(theme);
|
||||
domRelatedOptForTheme(theme);
|
||||
setTheme(theme);
|
||||
};
|
||||
|
||||
// 生产环境才统计
|
||||
useEffect(() => {
|
||||
console.log(import.meta.env.MODE);
|
||||
if (import.meta.env.PROD) {
|
||||
const busuanziScript = document.createElement('script');
|
||||
busuanziScript.src =
|
||||
'//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js';
|
||||
document.getElementById('root')?.appendChild(busuanziScript);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Game theme={theme} initLevel={initLevel} pureMode={!!theme.pure} />
|
||||
<BeiAn />
|
||||
{theme.background && (
|
||||
<img
|
||||
alt="background"
|
||||
src={theme.background}
|
||||
className="background"
|
||||
style={{
|
||||
filter: theme.backgroundBlur ? 'blur(8px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Title title={theme.title} desc={theme.desc} />
|
||||
<PersonalInfo />
|
||||
<Game
|
||||
key={theme.title}
|
||||
theme={theme}
|
||||
initLevel={initLevel}
|
||||
initScore={initScore}
|
||||
/>
|
||||
<div className={'flex-spacer'} />
|
||||
<p style={{ textAlign: 'center', fontSize: 10, opacity: 0.5 }}>
|
||||
<span id="busuanzi_container_site_pv">
|
||||
{' '}
|
||||
累计访问:
|
||||
<span id="busuanzi_value_site_pv" />次
|
||||
</span>
|
||||
<br />
|
||||
<BeiAn />
|
||||
</p>
|
||||
<Info />
|
||||
{!theme.pure && (
|
||||
<>
|
||||
<ThemeChanger
|
||||
changeTheme={changeTheme}
|
||||
onDiyClick={() => setDiyDialogShow(true)}
|
||||
/>
|
||||
<ConfigDialog
|
||||
show={diyDialogShow}
|
||||
closeMethod={() => setDiyDialogShow(false)}
|
||||
previewMethod={previewTheme}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,14 +2,12 @@ import React, { FC } from 'react';
|
|||
|
||||
export const BeiAn: FC = () => {
|
||||
return (
|
||||
<p style={{ textAlign: 'center' }}>
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
浙ICP备17007857号-2
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
浙ICP备17007857号-2
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { FC, useEffect, useRef, useState } from 'react';
|
|||
import style from './ConfigDialog.module.scss';
|
||||
import classNames from 'classnames';
|
||||
import { Icon, Sound, Theme } from '../themes/interface';
|
||||
import { defaultSounds } from '../themes/default';
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
import Bmob from 'hydrogen-js-sdk';
|
||||
import { captureElement } from '../utils';
|
||||
|
@ -226,25 +225,7 @@ export const ConfigDialog: FC<{
|
|||
if (!title) return Promise.reject('请填写标题');
|
||||
if (icons.length !== 10) return Promise.reject('图片素材需要提供10张');
|
||||
|
||||
let hasDefaultMaterial = false;
|
||||
const customIcons = icons.map((icon) => {
|
||||
if (!icon.clickSound) {
|
||||
hasDefaultMaterial = true;
|
||||
icon.clickSound = 'button-click';
|
||||
}
|
||||
if (!icon.tripleSound) {
|
||||
hasDefaultMaterial = true;
|
||||
icon.tripleSound = 'triple';
|
||||
}
|
||||
return { ...icon };
|
||||
});
|
||||
const customSounds = sounds.map((sounds) => ({ ...sounds }));
|
||||
if (hasDefaultMaterial) {
|
||||
customSounds.push(...defaultSounds);
|
||||
}
|
||||
|
||||
const customTheme: Theme<any> = {
|
||||
name: `自定义-${title}`,
|
||||
// 恭喜你发现纯净模式彩蛋🎉,点击文字十次可以开启纯净模式
|
||||
pure: pureCount !== 0 && pureCount % 10 === 0,
|
||||
title,
|
||||
|
@ -252,11 +233,13 @@ export const ConfigDialog: FC<{
|
|||
bgm,
|
||||
background,
|
||||
backgroundBlur,
|
||||
icons: customIcons,
|
||||
sounds: customSounds,
|
||||
icons,
|
||||
sounds,
|
||||
};
|
||||
|
||||
return Promise.resolve(customTheme);
|
||||
console.log(customTheme);
|
||||
|
||||
return Promise.resolve(JSON.parse(JSON.stringify(customTheme)));
|
||||
};
|
||||
|
||||
// 预览
|
||||
|
@ -335,11 +318,24 @@ export const ConfigDialog: FC<{
|
|||
>
|
||||
<p onClick={() => setPureCount(pureCount + 1)}>
|
||||
目前自定义仅支持配置https链接,可网上自行搜索素材复制链接,或者将自己处理好的素材上传第三方存储服务/图床上再复制外链
|
||||
(想白嫖的话自行搜索【免费图床】【免费对象存储】【免费mp3外链】等)
|
||||
(想白嫖的话自行搜索【免费图床】【免费对象存储】【免费mp3外链】等)。
|
||||
{pureCount != 0 &&
|
||||
pureCount % 10 === 0 &&
|
||||
'🎉🎉🎉恭喜发现彩蛋!主题分享后将开启纯净模式~'}
|
||||
</p>
|
||||
<div className="flex-container flex-no-wrap">
|
||||
<img
|
||||
style={{ width: 120, objectFit: 'contain' }}
|
||||
src="/wxqrcode.png"
|
||||
alt=""
|
||||
/>
|
||||
<p style={{ margin: 0 }}>
|
||||
<strong>
|
||||
开发不易,如果您喜欢这个项目的话可酌情扫左侧二维码
|
||||
请我喝杯咖啡(后台相关费用用爱发电中,感谢支持)
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/*基本配置*/}
|
||||
<h4 className="flex-container flex-center">
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
#root {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app {
|
||||
.game {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding-top: 10%;
|
||||
padding-bottom: 2.5%;
|
||||
}
|
||||
|
||||
.scene {
|
||||
&-container {
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
padding-bottom: 112.5%;
|
||||
position: relative;
|
||||
margin: 10% 0;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
|
@ -63,11 +57,21 @@
|
|||
}
|
||||
|
||||
.queue-container {
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
padding-bottom: 15%;
|
||||
border: 2px solid gray;
|
||||
padding-bottom: 18.75%;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 3.125%;
|
||||
right: 3.125%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 12px;
|
||||
background-color: rgb(0 0 0 / 16%);
|
||||
border: 8px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
|
@ -82,15 +86,17 @@
|
|||
background-color: rgb(0 0 0 / 10%);
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
.bgm-button {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
padding: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zhenghuo-button {
|
||||
|
@ -98,12 +104,9 @@
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
.level {
|
||||
font-size: 1.8em;
|
||||
font-weight: 900;
|
||||
line-height: 2;
|
||||
text-shadow: 4px 6px 2px rgb(0 0 0 / 20%);
|
||||
}
|
|
@ -7,8 +7,13 @@ import React, {
|
|||
} from 'react';
|
||||
|
||||
import './Game.scss';
|
||||
import { LAST_LEVEL_STORAGE_KEY, randomString, waitTimeout } from './utils';
|
||||
import { Icon, Theme } from './themes/interface';
|
||||
import {
|
||||
LAST_LEVEL_STORAGE_KEY,
|
||||
LAST_SCORE_STORAGE_KEY,
|
||||
randomString,
|
||||
waitTimeout,
|
||||
} from '../utils';
|
||||
import { Icon, Theme } from '../themes/interface';
|
||||
|
||||
// 最大关卡
|
||||
const maxLevel = 50;
|
||||
|
@ -156,12 +161,14 @@ const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
|
|||
const Game: FC<{
|
||||
theme: Theme<any>;
|
||||
initLevel: number;
|
||||
pureMode?: boolean;
|
||||
}> = ({ theme, initLevel, pureMode = false }) => {
|
||||
initScore: number;
|
||||
}> = ({ theme, initLevel, initScore }) => {
|
||||
console.log('Game FC');
|
||||
const [scene, setScene] = useState<Scene>(
|
||||
makeScene(initLevel, theme.icons)
|
||||
);
|
||||
const [level, setLevel] = useState<number>(initLevel);
|
||||
const [score, setScore] = useState<number>(initScore);
|
||||
const [queue, setQueue] = useState<MySymbol[]>([]);
|
||||
const [sortedQueue, setSortedQueue] = useState<
|
||||
Record<MySymbol['id'], number>
|
||||
|
@ -169,7 +176,6 @@ const Game: FC<{
|
|||
const [finished, setFinished] = useState<boolean>(false);
|
||||
const [tipText, setTipText] = useState<string>('');
|
||||
const [animating, setAnimating] = useState<boolean>(false);
|
||||
const [configDialogShow, setConfigDialogShow] = useState<boolean>(false);
|
||||
|
||||
// 音效
|
||||
const soundRefMap = useRef<Record<string, HTMLAudioElement>>({});
|
||||
|
@ -191,6 +197,7 @@ const Game: FC<{
|
|||
// 关卡缓存
|
||||
useEffect(() => {
|
||||
localStorage.setItem(LAST_LEVEL_STORAGE_KEY, level.toString());
|
||||
localStorage.setItem(LAST_SCORE_STORAGE_KEY, score.toString());
|
||||
}, [level]);
|
||||
|
||||
// 队列区排序
|
||||
|
@ -253,17 +260,20 @@ const Game: FC<{
|
|||
};
|
||||
|
||||
// 弹出
|
||||
const popTime = useRef(0);
|
||||
const pop = () => {
|
||||
if (!queue.length) return;
|
||||
const updateQueue = queue.slice();
|
||||
const symbol = updateQueue.shift();
|
||||
setScore(score - 1);
|
||||
if (!symbol) return;
|
||||
const find = scene.find((s) => s.id === symbol.id);
|
||||
if (find) {
|
||||
setQueue(updateQueue);
|
||||
find.status = 0;
|
||||
find.x = 100 * Math.floor(8 * Math.random());
|
||||
find.y = 700;
|
||||
find.x = 100 * (popTime.current % 7);
|
||||
popTime.current++;
|
||||
find.y = 800;
|
||||
checkCover(scene);
|
||||
// 音效
|
||||
if (soundRefMap.current?.['sound-shift']) {
|
||||
|
@ -276,6 +286,7 @@ const Game: FC<{
|
|||
// 撤销
|
||||
const undo = () => {
|
||||
if (!queue.length) return;
|
||||
setScore(score - 1);
|
||||
const updateQueue = queue.slice();
|
||||
const symbol = updateQueue.pop();
|
||||
if (!symbol) return;
|
||||
|
@ -294,6 +305,7 @@ const Game: FC<{
|
|||
|
||||
// 洗牌
|
||||
const wash = () => {
|
||||
setScore(score - 1);
|
||||
checkCover(washScene(level, scene));
|
||||
// 音效
|
||||
if (soundRefMap.current?.['sound-wash']) {
|
||||
|
@ -316,6 +328,7 @@ const Game: FC<{
|
|||
// 重开
|
||||
const restart = () => {
|
||||
setFinished(false);
|
||||
setScore(0);
|
||||
setLevel(1);
|
||||
setQueue([]);
|
||||
checkCover(makeScene(1, theme.icons));
|
||||
|
@ -358,6 +371,8 @@ const Game: FC<{
|
|||
|
||||
// 三连了
|
||||
if (filterSame.length === 3) {
|
||||
// 三连一次+3分
|
||||
setScore(score + 3);
|
||||
updateQueue = updateQueue.filter((sb) => sb.icon !== symbol.icon);
|
||||
for (const sb of filterSame) {
|
||||
const find = updateScene.find((i) => i.id === sb.id);
|
||||
|
@ -393,6 +408,8 @@ const Game: FC<{
|
|||
return;
|
||||
}
|
||||
// 升级
|
||||
// 通关奖励关卡对应数值分数
|
||||
setScore(score + level);
|
||||
setLevel(level + 1);
|
||||
setQueue([]);
|
||||
checkCover(makeScene(level + 1, theme.icons));
|
||||
|
@ -406,20 +423,7 @@ const Game: FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
{theme.background && (
|
||||
<img
|
||||
alt="background"
|
||||
src={theme.background}
|
||||
className="background"
|
||||
style={{
|
||||
filter: theme.backgroundBlur ? 'blur(8px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h3 className="flex-container flex-center">Level: {level}</h3>
|
||||
|
||||
<div className="app">
|
||||
<div className="game">
|
||||
<div className="scene-container">
|
||||
<div className="scene-inner">
|
||||
{scene.map((item, idx) => (
|
||||
|
@ -433,14 +437,14 @@ const Game: FC<{
|
|||
? sortedQueue[item.id]
|
||||
: -1000
|
||||
}
|
||||
y={item.status === 0 ? item.y : 895}
|
||||
y={item.status === 0 ? item.y : 945}
|
||||
onClick={() => clickSymbol(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="queue-container flex-container flex-center" />
|
||||
<div className="queue-container" />
|
||||
<div className="flex-container flex-between">
|
||||
<button className="flex-grow" onClick={pop}>
|
||||
弹出
|
||||
|
@ -451,10 +455,21 @@ const Game: FC<{
|
|||
<button className="flex-grow" onClick={wash}>
|
||||
洗牌
|
||||
</button>
|
||||
<button className="flex-grow" onClick={levelUp}>
|
||||
<button
|
||||
className="flex-grow"
|
||||
onClick={() => {
|
||||
// 跳关扣关卡对应数值的分
|
||||
setScore(score - level);
|
||||
levelUp();
|
||||
}}
|
||||
>
|
||||
下一关
|
||||
</button>
|
||||
</div>
|
||||
<div className="level">
|
||||
关卡{level}|剩余{scene.filter((i) => i.status === 0).length}
|
||||
|得分{score}
|
||||
</div>
|
||||
|
||||
{/*提示弹窗*/}
|
||||
{finished && (
|
||||
|
@ -465,14 +480,12 @@ const Game: FC<{
|
|||
)}
|
||||
|
||||
{/*bgm*/}
|
||||
<button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}>
|
||||
{bgmOn ? '🔊' : '🔈'}
|
||||
<audio
|
||||
ref={bgmRef}
|
||||
loop
|
||||
src={theme?.bgm || '/sound-disco.mp3'}
|
||||
/>
|
||||
</button>
|
||||
{theme.bgm && (
|
||||
<button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}>
|
||||
{bgmOn ? '🔊' : '🔈'}
|
||||
<audio ref={bgmRef} loop src={theme.bgm} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/*音效*/}
|
||||
{theme.sounds.map((sound) => (
|
77
src/components/Info.module.scss
Normal file
|
@ -0,0 +1,77 @@
|
|||
/* 可封装 */
|
||||
.info {
|
||||
position: fixed;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
transition: 0.3s;
|
||||
padding: 16px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background: rgb(0 0 0/ 50%);
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(8px);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
transition: 0.6s;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 2px;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(0 0 0/20%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
transform: scale(0);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.open {
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
width: calc(100% - 70px);
|
||||
}
|
||||
@media screen and (min-width: 501px) {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
p {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.close {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import React, { CSSProperties, FC } from 'react';
|
||||
|
||||
export const Info: FC<{ style?: CSSProperties }> = ({ style }) => {
|
||||
import React, { CSSProperties, FC, useState } from 'react';
|
||||
import style from './Info.module.scss';
|
||||
import classNames from 'classnames';
|
||||
export const Info: FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div style={style}>
|
||||
<p>
|
||||
<span id="busuanzi_container_site_pv">
|
||||
累计访问:<span id="busuanzi_value_site_pv"></span>次
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
onClick={() => !open && setOpen(true)}
|
||||
className={classNames(style.info, open && style.open)}
|
||||
>
|
||||
<div className={style.icon}>i</div>
|
||||
<p>
|
||||
bgm素材:
|
||||
<a
|
||||
|
@ -17,22 +18,6 @@ export const Info: FC<{ style?: CSSProperties }> = ({ style }) => {
|
|||
>
|
||||
洛天依,言和原创《普通DISCO》
|
||||
</a>
|
||||
、
|
||||
<a
|
||||
href="https://music.163.com/#/song?id=135022"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
贫民百万歌星
|
||||
</a>
|
||||
、
|
||||
<a
|
||||
href="https://y.qq.com/n/ryqq/songDetail/0020Nusb3QJGn9"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
只因你太美
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
玩法来源➡️羊了个羊➡️
|
||||
|
@ -44,6 +29,10 @@ export const Info: FC<{ style?: CSSProperties }> = ({ style }) => {
|
|||
3 Tiles
|
||||
</a>
|
||||
</p>
|
||||
<p>仅供交流,禁止商用</p>
|
||||
<div className={style.close} onClick={() => setOpen(false)}>
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
.loading {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@mixin position-top {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin position-right-top {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@mixin position-right {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@mixin position-right-bottom {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@mixin position-bottom {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin position-left-bottom {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@mixin position-left {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@mixin position-left-top {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% {
|
||||
@include position-left-top;
|
||||
}
|
||||
|
||||
12.5% {
|
||||
@include position-top;
|
||||
}
|
||||
|
||||
25% {
|
||||
@include position-right-top;
|
||||
}
|
||||
|
||||
37.5% {
|
||||
@include position-right;
|
||||
}
|
||||
|
||||
50% {
|
||||
@include position-right-bottom;
|
||||
}
|
||||
|
||||
62.5% {
|
||||
@include position-bottom;
|
||||
}
|
||||
|
||||
75% {
|
||||
@include position-left-bottom;
|
||||
}
|
||||
|
||||
87.5% {
|
||||
@include position-left;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
position: absolute;
|
||||
transition: 0.5s;
|
||||
border-radius: 12px;
|
||||
@include position-left-top;
|
||||
|
||||
&Container {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&1 {
|
||||
background-color: #646cff88;
|
||||
animation: move 1s infinite ease-in-out;
|
||||
|
||||
&.error {
|
||||
animation-play-state: paused;
|
||||
transform: rotate(75deg) translateX(30px);
|
||||
}
|
||||
}
|
||||
|
||||
&2 {
|
||||
background-color: #646cff66;
|
||||
animation: move 1s infinite ease-in-out;
|
||||
animation-delay: 0.375s;
|
||||
|
||||
&.error {
|
||||
animation-play-state: paused;
|
||||
transform: rotate(175deg) translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
&3 {
|
||||
background-color: #646cff44;
|
||||
animation: move 1s infinite ease-in-out;
|
||||
animation-delay: 0.75s;
|
||||
|
||||
&.error {
|
||||
animation-play-state: paused;
|
||||
transform: rotate(225deg) translateX(20px);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
import style from './Loading.module.scss';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Loading: FC<{ error: string }> = ({ error }) => {
|
||||
return (
|
||||
<div className={style.loading}>
|
||||
<div className={style.blockContainer}>
|
||||
{[1, 2, 3].map((num) => (
|
||||
<div
|
||||
key={num}
|
||||
className={classNames(
|
||||
style.block,
|
||||
style[`block${num}`],
|
||||
error && style.error
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{error ? (
|
||||
<span>
|
||||
{error},稍后再试或<a href="/">返回首页</a>
|
||||
</span>
|
||||
) : (
|
||||
<span>加载中...</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
137
src/components/PersonalInfo.module.scss
Normal file
|
@ -0,0 +1,137 @@
|
|||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
position: fixed;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
padding: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
animation: gradient 4s ease infinite;
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
#ee775288,
|
||||
#e73c7e88,
|
||||
#23a6d588,
|
||||
#23d5ab88
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
background-position: 0 0;
|
||||
box-sizing: border-box;
|
||||
transition: 0.4s;
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 9;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
|
||||
* {
|
||||
transition: 0.6s;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(0 0 0/20%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
transform: scale(0);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.github {
|
||||
&Icon {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&Link {
|
||||
position: absolute;
|
||||
right: -196px;
|
||||
top: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.bilibili {
|
||||
&Icon {
|
||||
position: absolute;
|
||||
right: -100px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
&Link {
|
||||
position: absolute;
|
||||
right: -196px;
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
@media screen and (min-width: 501px) {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.close {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.github {
|
||||
&Icon {
|
||||
right: calc(100% - 70px);
|
||||
top: 18px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
&Link {
|
||||
right: calc(100% - 200px);
|
||||
top: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.bilibili {
|
||||
&Icon {
|
||||
height: 36px;
|
||||
right: 26px;
|
||||
top: 50px;
|
||||
}
|
||||
|
||||
&Link {
|
||||
right: 110px;
|
||||
top: 58px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
src/components/ThemeChanger.module.scss
Normal file
|
@ -0,0 +1,48 @@
|
|||
.container {
|
||||
position: fixed;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
transition: 0.3s;
|
||||
color: white;
|
||||
backdrop-filter: blur(8px);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
line-height: 52px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
overflow: visible;
|
||||
|
||||
.square {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 8px;
|
||||
background: rgb(0 0 0/ 40%);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
.diy {
|
||||
width: 80px;
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
#ee7752,
|
||||
#e73c7e,
|
||||
#23a6d5,
|
||||
#23d5ab
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
76
src/components/ThemeChanger.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
import style from './ThemeChanger.module.scss';
|
||||
import classNames from 'classnames';
|
||||
import { fishermanTheme } from '../themes/fisherman';
|
||||
import { jinlunTheme } from '../themes/jinlun';
|
||||
import { ikunTheme } from '../themes/ikun';
|
||||
import { pddTheme } from '../themes/pdd';
|
||||
import { getDefaultTheme } from '../themes/default';
|
||||
import { Theme } from '../themes/interface';
|
||||
|
||||
const BuiltinThemes = [
|
||||
getDefaultTheme(),
|
||||
fishermanTheme,
|
||||
jinlunTheme,
|
||||
ikunTheme,
|
||||
pddTheme,
|
||||
];
|
||||
|
||||
export const ThemeChanger: FC<{
|
||||
changeTheme: (theme: Theme<any>) => void;
|
||||
onDiyClick: () => void;
|
||||
}> = ({ changeTheme, onDiyClick }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div className={classNames(style.container, open && style.open)}>
|
||||
{BuiltinThemes.map((theme, idx) => (
|
||||
<div
|
||||
className={classNames(style.square)}
|
||||
key={theme.title}
|
||||
style={{
|
||||
opacity: open ? 1 : 0.3,
|
||||
transform: open
|
||||
? `translateY(-${110 * (idx + 1)}%)`
|
||||
: '',
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
changeTheme(theme);
|
||||
}}
|
||||
>
|
||||
{typeof theme.icons[0].content === 'string' ? (
|
||||
theme.icons[0].content.startsWith('http') ? (
|
||||
/*图片外链*/
|
||||
<img src={theme.icons[0].content} alt="" />
|
||||
) : (
|
||||
/*字符表情*/
|
||||
<i>{theme.icons[0].content}</i>
|
||||
)
|
||||
) : (
|
||||
/*ReactNode*/
|
||||
theme.icons[0].content
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={classNames(style.square, style.diy)}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onDiyClick();
|
||||
}}
|
||||
style={{
|
||||
opacity: open ? 1 : 0.3,
|
||||
transform: open ? `translateY(-${110 * 6}%)` : '',
|
||||
}}
|
||||
>
|
||||
DIY!
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setOpen(!open)}
|
||||
className={classNames(style.square)}
|
||||
>
|
||||
{open ? '收起' : '更多'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
63
src/components/Title.module.scss
Normal file
|
@ -0,0 +1,63 @@
|
|||
@keyframes jump {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 2rem;
|
||||
padding-top: 1rem;
|
||||
animation: scale 0.27s infinite alternate;
|
||||
z-index: 1;
|
||||
|
||||
* {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: inline-block;
|
||||
animation-name: jump;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: 3s;
|
||||
animation-play-state: running;
|
||||
text-shadow: 4px 6px 2px rgb(0 0 0 / 20%);
|
||||
}
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding-top: 0.5rem;
|
||||
}
|
24
src/components/Title.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { FC } from 'react';
|
||||
import style from './Title.module.scss';
|
||||
|
||||
export const Title: FC<{ title: string; desc?: string }> = ({
|
||||
title,
|
||||
desc,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h1 className={style.title}>
|
||||
{[...title].map((str, i) => (
|
||||
<span
|
||||
className={style.item}
|
||||
style={{ animationDelay: i / 10 + 's' }}
|
||||
key={`${i}`}
|
||||
>
|
||||
{str}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
{desc && <h2 className={style.description}>{desc}</h2>}
|
||||
</>
|
||||
);
|
||||
};
|
101
src/main.tsx
|
@ -4,6 +4,51 @@ import App from './App';
|
|||
import './styles/global.scss';
|
||||
import './styles/utils.scss';
|
||||
import Bmob from 'hydrogen-js-sdk';
|
||||
import {
|
||||
DEFAULT_BGM_STORAGE_KEY,
|
||||
domRelatedOptForTheme,
|
||||
parsePathCustomThemeId,
|
||||
wrapThemeDefaultSounds,
|
||||
} from './utils';
|
||||
import { getDefaultTheme } from './themes/default';
|
||||
import { Theme } from './themes/interface';
|
||||
|
||||
// react渲染
|
||||
const render = (theme: Theme<any>) => {
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App theme={theme} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
};
|
||||
|
||||
// 错误提示
|
||||
const errorTip = (tip: string) => {
|
||||
setTimeout(() => {
|
||||
document.getElementById('loading')?.classList.add('error');
|
||||
document.getElementById('loadingText')!.innerText = tip;
|
||||
document.getElementById('backHomeTip')!.style.visibility = 'visible';
|
||||
}, 600);
|
||||
};
|
||||
|
||||
// 加载成功后数据转换(runtime)以及转场
|
||||
const successTrans = (theme: Theme<any>) => {
|
||||
wrapThemeDefaultSounds(theme);
|
||||
|
||||
setTimeout(() => {
|
||||
domRelatedOptForTheme(theme);
|
||||
const root = document.getElementById('root');
|
||||
root!.style.opacity = '0';
|
||||
document.getElementById('loading')?.classList.add('success');
|
||||
setTimeout(() => {
|
||||
render(theme);
|
||||
root!.style.opacity = '1';
|
||||
}, 600);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 从url初始化主题
|
||||
const customThemeIdFromPath = parsePathCustomThemeId(location.href);
|
||||
|
||||
// Bmob初始化
|
||||
// @ts-ignore
|
||||
|
@ -12,8 +57,54 @@ Bmob.initialize(
|
|||
import.meta.env.VITE_BMOB_SECCODE
|
||||
);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
const loadTheme = () => {
|
||||
// 请求主题
|
||||
if (customThemeIdFromPath) {
|
||||
const storageTheme = localStorage.getItem(customThemeIdFromPath);
|
||||
if (storageTheme) {
|
||||
try {
|
||||
const customTheme = JSON.parse(storageTheme);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
} else {
|
||||
Bmob.Query('config')
|
||||
.get(customThemeIdFromPath)
|
||||
.then((res) => {
|
||||
const { content } = res as any;
|
||||
localStorage.setItem(customThemeIdFromPath, content);
|
||||
try {
|
||||
const customTheme = JSON.parse(content);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
successTrans(getDefaultTheme());
|
||||
}
|
||||
};
|
||||
|
||||
// 音效资源请求
|
||||
if (!localStorage.getItem(DEFAULT_BGM_STORAGE_KEY)) {
|
||||
const query = Bmob.Query('file');
|
||||
query.equalTo('type', '==', 'default');
|
||||
query
|
||||
.find()
|
||||
.then((results) => {
|
||||
for (const file of results as any) {
|
||||
localStorage.setItem(file.name, file.base64);
|
||||
}
|
||||
loadTheme();
|
||||
})
|
||||
.catch((e) => {
|
||||
errorTip(e);
|
||||
});
|
||||
} else {
|
||||
loadTheme();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color-scheme: light dark;
|
||||
color: rgb(255 255 255 / 87%);
|
||||
background-color: #242424;
|
||||
body {
|
||||
padding: 0 32px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
@ -13,24 +8,26 @@
|
|||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
#root {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
button {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #3338;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
&.primary {
|
||||
background-color: #747bff;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
|
@ -45,40 +42,4 @@ input {
|
|||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
word-break: keep-all;
|
||||
outline: none;
|
||||
|
||||
&.primary {
|
||||
background-color: #646cff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover:not(.primary) {
|
||||
border-color: #646cff;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-no-wrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Theme } from '../interface';
|
||||
import {
|
||||
DEFAULT_BGM_STORAGE_KEY,
|
||||
DEFAULT_CLICK_SOUND_STORAGE_KEY,
|
||||
DEFAULT_TRIPLE_SOUND_STORAGE_KEY,
|
||||
} from '../../utils';
|
||||
|
||||
const icons = <const>[
|
||||
`🎨`,
|
||||
|
@ -15,27 +20,31 @@ const icons = <const>[
|
|||
|
||||
export type DefaultSoundNames = 'button-click' | 'triple';
|
||||
|
||||
import soundButtonClickUrl from './sounds/sound-button-click.mp3';
|
||||
import soundTripleUrl from './sounds/sound-triple.mp3';
|
||||
export const defaultSounds: Theme<DefaultSoundNames>['sounds'] = [
|
||||
{
|
||||
name: 'button-click',
|
||||
src: soundButtonClickUrl,
|
||||
},
|
||||
{
|
||||
name: 'triple',
|
||||
src: soundTripleUrl,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultTheme: Theme<DefaultSoundNames> = {
|
||||
title: '有解的羊了个羊(DEMO)',
|
||||
name: '默认',
|
||||
icons: icons.map((icon) => ({
|
||||
name: icon,
|
||||
content: icon,
|
||||
clickSound: 'button-click',
|
||||
tripleSound: 'triple',
|
||||
})),
|
||||
sounds: defaultSounds,
|
||||
export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
|
||||
return {
|
||||
title: '有解的羊了个羊',
|
||||
desc: '真的可以通关~',
|
||||
dark: true,
|
||||
backgroundColor: '#8dac85',
|
||||
icons: icons.map((icon) => ({
|
||||
name: icon,
|
||||
content: icon,
|
||||
clickSound: 'button-click',
|
||||
tripleSound: 'triple',
|
||||
})),
|
||||
sounds: [
|
||||
{
|
||||
name: 'button-click',
|
||||
src:
|
||||
localStorage.getItem(DEFAULT_CLICK_SOUND_STORAGE_KEY) || '',
|
||||
},
|
||||
{
|
||||
name: 'triple',
|
||||
src:
|
||||
localStorage.getItem(DEFAULT_TRIPLE_SOUND_STORAGE_KEY) ||
|
||||
'',
|
||||
},
|
||||
],
|
||||
bgm: localStorage.getItem(DEFAULT_BGM_STORAGE_KEY) || '',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// 钓鱼佬主题
|
||||
import React from 'react';
|
||||
import { Theme } from '../interface';
|
||||
import { DefaultSoundNames, defaultSounds } from '../default';
|
||||
import { DefaultSoundNames } from '../default';
|
||||
|
||||
const imagesUrls = import.meta.glob('./images/*.png', {
|
||||
import: 'default',
|
||||
|
@ -17,12 +17,11 @@ const fishes = Object.entries(imagesUrls).map(([key, value]) => ({
|
|||
|
||||
export const fishermanTheme: Theme<DefaultSoundNames> = {
|
||||
title: '🐟鱼了个鱼🐟',
|
||||
name: '钓鱼佬',
|
||||
icons: fishes.map(({ name, content }) => ({
|
||||
name,
|
||||
content,
|
||||
clickSound: 'button-click',
|
||||
tripleSound: 'triple',
|
||||
})),
|
||||
sounds: defaultSounds,
|
||||
sounds: [],
|
||||
};
|
||||
|
|
|
@ -53,7 +53,6 @@ const icons = Object.entries(imagesUrls).map(([key, value]) => ({
|
|||
|
||||
export const ikunTheme: Theme<SoundNames> = {
|
||||
title: '🐔鸡了个鸡🐔',
|
||||
name: 'iKun',
|
||||
bgm,
|
||||
icons: icons.map(({ name, content }) => ({
|
||||
name,
|
||||
|
|
|
@ -14,14 +14,14 @@ export interface Sound<T = string> {
|
|||
|
||||
type Operation = 'shift' | 'undo' | 'wash';
|
||||
|
||||
// TODO title name 冗余
|
||||
export interface Theme<SoundNames> {
|
||||
title: string;
|
||||
desc?: ReactNode;
|
||||
name: string;
|
||||
desc?: string;
|
||||
bgm?: string;
|
||||
background?: string;
|
||||
backgroundColor?: string;
|
||||
backgroundBlur?: boolean;
|
||||
dark?: boolean;
|
||||
pure?: boolean;
|
||||
icons: Icon<SoundNames>[];
|
||||
sounds: Sound<SoundNames>[];
|
||||
|
|
|
@ -25,7 +25,6 @@ const icons = Object.entries(imagesUrls).map(([key, value]) => ({
|
|||
|
||||
export const jinlunTheme: Theme<string> = {
|
||||
title: '🐎马了个马🐎',
|
||||
name: '金轮',
|
||||
icons: icons.map(({ name, content }) => ({
|
||||
name,
|
||||
content,
|
||||
|
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 92 KiB |
|
@ -1,50 +0,0 @@
|
|||
import { Theme } from '../interface';
|
||||
import React from 'react';
|
||||
import { defaultSounds } from '../default';
|
||||
|
||||
const soundUrls = import.meta.glob('./sounds/*.mp3', {
|
||||
import: 'default',
|
||||
eager: true,
|
||||
});
|
||||
|
||||
const sounds = Object.entries(soundUrls).map(([key, value]) => ({
|
||||
name: key.slice(9, -4),
|
||||
src: value,
|
||||
})) as Theme<string>['sounds'];
|
||||
|
||||
const imagesUrls = import.meta.glob('./images/*.png', {
|
||||
import: 'default',
|
||||
eager: true,
|
||||
});
|
||||
|
||||
const icons = Object.entries(imagesUrls).map(([key, value]) => ({
|
||||
name: key.slice(9, -4),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
content: <img src={value} alt="" />,
|
||||
}));
|
||||
|
||||
export const owTheme: Theme<string> = {
|
||||
title: '守望先锋',
|
||||
desc: (
|
||||
<p>
|
||||
感谢
|
||||
<a
|
||||
href="https://space.bilibili.com/228122468"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
白板特工华南虎
|
||||
</a>
|
||||
提供素材
|
||||
</p>
|
||||
),
|
||||
name: 'OW',
|
||||
icons: icons.map(({ name, content }) => ({
|
||||
name,
|
||||
content,
|
||||
clickSound: 'button-click',
|
||||
tripleSound: name === 'ow' ? 'triple' : name,
|
||||
})),
|
||||
sounds: [...defaultSounds, ...sounds],
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
// 骚猪主题
|
||||
import React from 'react';
|
||||
import { Theme } from '../interface';
|
||||
import { defaultSounds } from '../default';
|
||||
import bgm from './sounds/bgm.mp3';
|
||||
|
||||
const soundUrls = import.meta.glob('./sounds/*.mp3', {
|
||||
|
@ -28,20 +27,7 @@ const images = Object.entries(imagesUrls).map(([key, value]) => ({
|
|||
|
||||
export const pddTheme: Theme<string> = {
|
||||
title: '🐷猪了个猪🐷',
|
||||
desc: (
|
||||
<p>
|
||||
感谢
|
||||
<a
|
||||
href="https://space.bilibili.com/81966051"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
猪酱的日常
|
||||
</a>
|
||||
提供素材
|
||||
</p>
|
||||
),
|
||||
name: '骚猪',
|
||||
desc: '感谢 @猪酱的日常 提供素材',
|
||||
bgm: bgm,
|
||||
icons: images.map(({ name, content }) => ({
|
||||
name,
|
||||
|
@ -49,5 +35,5 @@ export const pddTheme: Theme<string> = {
|
|||
clickSound: 'button-click',
|
||||
tripleSound: name,
|
||||
})),
|
||||
sounds: [defaultSounds[0], ...sounds],
|
||||
sounds,
|
||||
};
|
||||
|
|
62
src/utils.ts
|
@ -1,3 +1,12 @@
|
|||
import { Theme } from './themes/interface';
|
||||
import { getDefaultTheme } from './themes/default';
|
||||
|
||||
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
|
||||
export const LAST_SCORE_STORAGE_KEY = 'lastScore';
|
||||
export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm';
|
||||
export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound';
|
||||
export const DEFAULT_CLICK_SOUND_STORAGE_KEY = 'defaultClickSound';
|
||||
|
||||
export const randomString: (len: number) => string = (len) => {
|
||||
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let res = '';
|
||||
|
@ -16,14 +25,7 @@ export const waitTimeout: (timeout: number) => Promise<void> = (timeout) => {
|
|||
});
|
||||
};
|
||||
|
||||
// 从url获取内置主题name
|
||||
export const parsePathThemeName: (url: string) => string = (url) => {
|
||||
const urlObj = new URL(url);
|
||||
const params = urlObj.searchParams;
|
||||
return decodeURIComponent(params.get('theme') || '默认');
|
||||
};
|
||||
|
||||
// 从url解析自定义主题JSON
|
||||
// 从url获取自定义主题Id
|
||||
export const parsePathCustomThemeId: (url: string) => string = (url) => {
|
||||
const urlObj = new URL(url);
|
||||
const params = urlObj.searchParams;
|
||||
|
@ -62,4 +64,46 @@ export const captureElement = (id: string, filename: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
|
||||
export const wrapThemeDefaultSounds: (theme: Theme<any>) => void = (theme) => {
|
||||
const defaultTheme = getDefaultTheme();
|
||||
// 默认音频资源补充
|
||||
if (!theme.bgm) {
|
||||
theme.bgm = defaultTheme.bgm;
|
||||
}
|
||||
let hasUseDefaultTriple, hasUseDefaultClick;
|
||||
for (const icon of theme.icons) {
|
||||
if (!icon.clickSound) icon.clickSound = 'button-click';
|
||||
if (!icon.tripleSound) icon.tripleSound = 'triple';
|
||||
if (icon.clickSound === 'button-click') hasUseDefaultTriple = true;
|
||||
if (icon.tripleSound === 'triple') hasUseDefaultClick = true;
|
||||
}
|
||||
if (
|
||||
hasUseDefaultClick &&
|
||||
!theme.sounds.find((s) => s.name === 'button-click')
|
||||
) {
|
||||
const defaultClick = defaultTheme.sounds.find(
|
||||
(s) => s.name === 'button-click'
|
||||
);
|
||||
defaultClick && theme.sounds.push(defaultClick);
|
||||
}
|
||||
if (hasUseDefaultTriple && !theme.sounds.find((s) => s.name === 'triple')) {
|
||||
const defaultTripleSound = defaultTheme.sounds.find(
|
||||
(s) => s.name === 'triple'
|
||||
);
|
||||
defaultTripleSound && theme.sounds.push(defaultTripleSound);
|
||||
}
|
||||
|
||||
// 兼容旧数据
|
||||
for (const sound of theme.sounds) {
|
||||
if (['triple', 'button-click'].includes(sound.name))
|
||||
// @ts-ignore
|
||||
sound.src = defaultTheme.sounds.find(
|
||||
(s) => s.name === sound.name
|
||||
).src;
|
||||
}
|
||||
};
|
||||
|
||||
export const domRelatedOptForTheme = (theme: Theme<any>) => {
|
||||
document.body.style.backgroundColor = theme.backgroundColor || 'white';
|
||||
document.body.style.color = theme.dark ? 'white' : 'rgb(0 0 0 / 60%)';
|
||||
};
|
||||
|
|