mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-30 03:08:15 +08:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
09e43ae02a | ||
![]() |
c5bd3f01d9 | ||
![]() |
be0faff9fd | ||
![]() |
cebf64847f | ||
![]() |
ffbb38495f | ||
![]() |
e553e5360f | ||
![]() |
d234bd06a8 | ||
![]() |
680811c50c | ||
![]() |
ffabf6805f | ||
![]() |
58bff1ce98 | ||
![]() |
cd5501ebea | ||
![]() |
331b96c8f0 | ||
![]() |
c6671038a8 | ||
![]() |
5579d5a32e | ||
![]() |
8dec5b6420 | ||
![]() |
3913982716 | ||
![]() |
5353a40416 |
0
.husky/commit-msg
Executable file → Normal file
0
.husky/commit-msg
Executable file → Normal file
0
.husky/pre-commit
Executable file → Normal file
0
.husky/pre-commit
Executable file → Normal file
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -2,6 +2,32 @@
|
|||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [1.1.0](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.1...v1.1.0) (2023-05-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 本地图片文件压缩 ([ffbb384](https://github.com/StreakingMan/solvable-sheep-game/commit/ffbb38495f2c8c124f6039496f613e6c416d9db4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 图片文件大小校验逻辑 ([be0faff](https://github.com/StreakingMan/solvable-sheep-game/commit/be0faff9fdf4aea6a2659325520d68b2f348c656))
|
||||
* 自定义主题编辑回显 ([ffabf68](https://github.com/StreakingMan/solvable-sheep-game/commit/ffabf6805f81dd03bd070dd09a32e51a51d1138f))
|
||||
* 音频文件切换为minio外链 ([680811c](https://github.com/StreakingMan/solvable-sheep-game/commit/680811c50cbef3fec80b73407daddcf48847f3bd))
|
||||
|
||||
### [1.0.1](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.0...v1.0.1) (2022-10-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* diy模式得分页再来一次按钮显示 ([c667103](https://github.com/StreakingMan/solvable-sheep-game/commit/c6671038a831fd069b930303aa3bc841b5317571))
|
||||
* 主题配置表单样式优化 ([5579d5a](https://github.com/StreakingMan/solvable-sheep-game/commit/5579d5a32e87a37c50d33621551dd39f7a0c74e5))
|
||||
* 主题配置表单样式优化 ([8dec5b6](https://github.com/StreakingMan/solvable-sheep-game/commit/8dec5b6420b256ee6d87689d6790d4da81171913))
|
||||
* 排行榜滚动定位 ([5353a40](https://github.com/StreakingMan/solvable-sheep-game/commit/5353a4041615589ac57d0cd8ce1ebca49372211c))
|
||||
* 纯净模式隐藏二维码 ([3913982](https://github.com/StreakingMan/solvable-sheep-game/commit/3913982716d570d11afbeacdad8dd5d6d522eb37))
|
||||
* 通关成功状态判断,完成游戏后无法点击问题 ([331b96c](https://github.com/StreakingMan/solvable-sheep-game/commit/331b96c8f0717bbb7912984b38cfde8622ea9bc6))
|
||||
|
||||
## [1.0.0](https://github.com/StreakingMan/solvable-sheep-game/compare/v0.0.10...v1.0.0) (2022-10-12)
|
||||
|
||||
|
||||
|
|
|
@ -76,7 +76,9 @@ vite+react 实现,欢迎 star、issue、pr、fork(尽量标注原仓库地
|
|||
|
||||
## 资助
|
||||
|
||||
由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。
|
||||
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/分摊后台服务费用~ 😘
|
||||
~~由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。~~
|
||||
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/~~分摊后台服务费用~~~ 😘
|
||||
|
||||
2023.5.5 更新:Bmob 服务到期,后台服务已下线,相关功能暂时无法使用,如有需要请自行搭建后台服务
|
||||
|
||||

|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
游戏的核心逻辑已经封装到了 `src/components/Game.tsx` ,方便大家魔改, 主题配置的类型声明见 `src/themes/interface.ts`
|
||||
|
||||
你可以先通过超酷的[stackblitz](https://stackblitz.com/edit/solvable-sheep-game?file=diy%2Fdiy.theme.json&terminal=dev:diy)
|
||||
在线体验一番(等待依赖安装完成后,编辑配置将实时更新)再回来看这里的指南。
|
||||
|
||||
## 准备工作
|
||||
|
||||
### 环境准备
|
||||
|
@ -68,8 +71,8 @@ yarn dev:diy
|
|||
yarn build:diy
|
||||
```
|
||||
|
||||
会在 `diy/diy-dist` 下生成静态资源,直接将这些文件复制服务器上做代理即可。如果嫌麻烦,推荐使用 [vercel](https://vercel.com/)
|
||||
一键部署(每月免费 100G 流量), 将更改后的项目推到自己的 github(gitlab,bitbucket 同样支持)仓库,
|
||||
会在 `diy/diy-dist` 下生成静态资源,直接将这些文件复制服务器上做代理即可。如果嫌麻烦,推荐使用 [vercel](https://vercel.com/)
|
||||
一键部署(每月免费 100G 流量), 将更改后的项目推到自己的 github(gitlab,bitbucket 同样支持)仓库,
|
||||
使用 github 账号登录 vercel 后导入该项目,构建模版选择 vite,
|
||||
构建命令更改为 `yarn build:diy` 输出地址改为 `diy/diy-dist` 即可 。见下图:
|
||||
|
||||
|
@ -79,7 +82,7 @@ yarn build:diy
|
|||
|
||||
## 其他
|
||||
|
||||
如果您想体验项目的完整功能,则需要注册一个 [Bmob](https://www.bmobapp.com/) 账号,
|
||||
如果您想体验项目的完整功能,则需要注册一个 [Bmob](https://www.bmobapp.com/) 账号,
|
||||
注册后新建应用(有一年的白嫖版,免费请求数虽然很客观,但并发数有限制,请根据自己的实际流量
|
||||
选择升级套餐,或者其他存储方案)
|
||||
|
||||
|
@ -94,9 +97,6 @@ ps: 如果您的项目托管在公共仓库中,请注意保护密钥,本地
|
|||
|
||||
`config` 表用来存储自定义配置的 json 字符串,需要新增 `content` 列
|
||||
|
||||
`file` 表则是为了节省 vercel 流量,将一些默认文件转为 base64 字符串存到了数据库中,需要添加三列
|
||||

|
||||
|
||||
`rank` 表,储存排名信息
|
||||

|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "solvable-sheep-game",
|
||||
"private": false,
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
@ -90,7 +90,7 @@ const App: FC<{ theme: Theme<any> }> = ({ theme: initTheme }) => {
|
|||
<PersonalInfo />
|
||||
<div className={'flex-spacer'} style={{ minHeight: 52 }} />
|
||||
<Suspense fallback={<span>Loading</span>}>
|
||||
{!__DIY__ && <WxQrCode />}
|
||||
{!__DIY__ && !theme.pure && <WxQrCode />}
|
||||
</Suspense>
|
||||
{!__DIY__ && (
|
||||
<p
|
||||
|
|
|
@ -146,7 +146,8 @@
|
|||
|
||||
input,
|
||||
select {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
randomString,
|
||||
wrapThemeDefaultSounds,
|
||||
LAST_UPLOAD_TIME_STORAGE_KEY,
|
||||
canvasToFile,
|
||||
createCanvasByImgSrc,
|
||||
} from '../utils';
|
||||
import { copy } from 'clipboard';
|
||||
import { CloseIcon } from './CloseIcon';
|
||||
|
@ -65,6 +67,7 @@ const ConfigDialog: FC<{
|
|||
const [customTheme, setCustomTheme] = useState<CustomTheme>({
|
||||
title: '',
|
||||
sounds: [],
|
||||
pure: false,
|
||||
icons: new Array(10).fill(0).map(() => ({
|
||||
name: randomString(4),
|
||||
content: '',
|
||||
|
@ -143,10 +146,14 @@ const ConfigDialog: FC<{
|
|||
new Array(10).fill('')
|
||||
);
|
||||
// 文件体积校验开关
|
||||
const initEnableFileSizeValidate = localStorage.getItem(
|
||||
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY
|
||||
);
|
||||
const [enableFileSizeValidate, setEnableFileSizeValidate] =
|
||||
useState<boolean>(
|
||||
localStorage.getItem(CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY) !==
|
||||
'false'
|
||||
initEnableFileSizeValidate === null
|
||||
? true
|
||||
: initEnableFileSizeValidate === 'true'
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
|
@ -162,7 +169,7 @@ const ConfigDialog: FC<{
|
|||
type: 'bgm' | 'background' | 'sound' | 'icon';
|
||||
file?: File;
|
||||
idx?: number;
|
||||
}) => void = ({ type, file, idx }) => {
|
||||
}) => void = async ({ type, file, idx }) => {
|
||||
if (!file) return;
|
||||
switch (type) {
|
||||
case 'bgm':
|
||||
|
@ -180,16 +187,20 @@ const ConfigDialog: FC<{
|
|||
break;
|
||||
case 'background':
|
||||
setBackgroundError('');
|
||||
if (enableFileSizeValidate && file.size > 80 * 1024) {
|
||||
return setBackgroundError('请选择80k以内全损画质的图片');
|
||||
try {
|
||||
const _file = enableFileSizeValidate
|
||||
? await canvasToFile({
|
||||
canvas: await createCanvasByImgSrc({
|
||||
imgSrc: await getFileBase64String(file),
|
||||
}),
|
||||
maxFileSize: 20 * 1024,
|
||||
})
|
||||
: file;
|
||||
const fileBase64 = await getFileBase64String(_file);
|
||||
updateCustomTheme('background', fileBase64);
|
||||
} catch (e: any) {
|
||||
setBackgroundError(e);
|
||||
}
|
||||
getFileBase64String(file)
|
||||
.then((res) => {
|
||||
updateCustomTheme('background', res);
|
||||
})
|
||||
.catch((e) => {
|
||||
setBackgroundError(e);
|
||||
});
|
||||
break;
|
||||
case 'sound':
|
||||
setSoundError('');
|
||||
|
@ -207,23 +218,27 @@ const ConfigDialog: FC<{
|
|||
case 'icon':
|
||||
if (idx == null) return;
|
||||
setIconErrors(makeIconErrors(idx, ''));
|
||||
if (enableFileSizeValidate && file.size > 5 * 1024) {
|
||||
return setIconErrors(
|
||||
makeIconErrors(idx, '请选择5k以内的图片文件')
|
||||
try {
|
||||
const _file = enableFileSizeValidate
|
||||
? await canvasToFile({
|
||||
canvas: await createCanvasByImgSrc({
|
||||
imgSrc: await getFileBase64String(file),
|
||||
}),
|
||||
maxFileSize: 4 * 1024,
|
||||
})
|
||||
: file;
|
||||
const fileBase64 = await getFileBase64String(_file);
|
||||
updateCustomTheme(
|
||||
'icons',
|
||||
customTheme.icons.map((icon, _idx) =>
|
||||
_idx === idx
|
||||
? { ...icon, content: fileBase64 }
|
||||
: icon
|
||||
)
|
||||
);
|
||||
} catch (e: any) {
|
||||
setIconErrors(makeIconErrors(idx, e));
|
||||
}
|
||||
getFileBase64String(file)
|
||||
.then((res) => {
|
||||
updateCustomTheme(
|
||||
'icons',
|
||||
customTheme.icons.map((icon, _idx) =>
|
||||
_idx === idx ? { ...icon, content: res } : icon
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
setIconErrors(makeIconErrors(idx, e));
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -250,7 +265,9 @@ const ConfigDialog: FC<{
|
|||
if (configString) {
|
||||
const parseRes = JSON.parse(configString);
|
||||
if (typeof parseRes === 'object') {
|
||||
setCustomTheme(parseRes);
|
||||
setTimeout(() => {
|
||||
setCustomTheme(parseRes);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -356,11 +373,7 @@ const ConfigDialog: FC<{
|
|||
})
|
||||
.catch(({ error, code }) => {
|
||||
setTimeout(() => {
|
||||
setConfigError(
|
||||
code === 10007
|
||||
? '上传数据长度已超过bmob的限制'
|
||||
: error
|
||||
);
|
||||
setConfigError(error);
|
||||
}, 3000);
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -375,12 +388,32 @@ const ConfigDialog: FC<{
|
|||
});
|
||||
};
|
||||
|
||||
// 彩蛋
|
||||
const [pureClickTime, setPureClickTime] = useState<number>(0);
|
||||
useEffect(() => {
|
||||
updateCustomTheme(
|
||||
'pure',
|
||||
pureClickTime % 5 === 0 && pureClickTime !== 0
|
||||
);
|
||||
}, [pureClickTime]);
|
||||
|
||||
return (
|
||||
<div className={classNames(style.dialog)}>
|
||||
<div className={style.closeBtn} onClick={closeMethod}>
|
||||
<CloseIcon fill={'#fff'} />
|
||||
</div>
|
||||
<h2>自定义主题</h2>
|
||||
<p style={{ color: 'red' }}>
|
||||
后台服务到期,分享链接功能已停用,有需要的同学参考
|
||||
<a
|
||||
href="https://github.com/StreakingMan/solvable-sheep-game/blob/master/diy/README.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
👉这里👈
|
||||
</a>
|
||||
自行部署,感谢支持!🙏
|
||||
</p>
|
||||
|
||||
<InputContainer label={'标题'} required>
|
||||
<input
|
||||
|
@ -398,7 +431,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'BGM'}>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -420,7 +453,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'背景图'}>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -451,36 +484,43 @@ const ConfigDialog: FC<{
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={'flex-container flex-center flex-no-wrap'}>
|
||||
<span>毛玻璃</span>
|
||||
<input
|
||||
type={'checkbox'}
|
||||
checked={!!customTheme.backgroundBlur}
|
||||
onChange={(e) =>
|
||||
updateCustomTheme(
|
||||
'backgroundBlur',
|
||||
e.target.checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className={'flex-spacer'} />
|
||||
<span>深色</span>
|
||||
<input
|
||||
type={'checkbox'}
|
||||
checked={!!customTheme.dark}
|
||||
onChange={(e) =>
|
||||
updateCustomTheme('dark', e.target.checked)
|
||||
}
|
||||
/>
|
||||
<div className={'flex-spacer'} />
|
||||
<span>纯色</span>
|
||||
<input
|
||||
type={'color'}
|
||||
value={customTheme.backgroundColor || '#ffffff'}
|
||||
onChange={(e) =>
|
||||
updateCustomTheme('backgroundColor', e.target.value)
|
||||
}
|
||||
/>
|
||||
<div className={'flex-container flex-center flex-wrap'}>
|
||||
<div className={'flex-spacer flex-container flex-center'}>
|
||||
<span>毛玻璃</span>
|
||||
<input
|
||||
type={'checkbox'}
|
||||
checked={!!customTheme.backgroundBlur}
|
||||
onChange={(e) =>
|
||||
updateCustomTheme(
|
||||
'backgroundBlur',
|
||||
e.target.checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex-spacer flex-container flex-center'}>
|
||||
<span>深色</span>
|
||||
<input
|
||||
type={'checkbox'}
|
||||
checked={!!customTheme.dark}
|
||||
onChange={(e) =>
|
||||
updateCustomTheme('dark', e.target.checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex-spacer flex-container flex-center'}>
|
||||
<span>纯色</span>
|
||||
<input
|
||||
type={'color'}
|
||||
value={customTheme.backgroundColor || '#ffffff'}
|
||||
onChange={(e) =>
|
||||
updateCustomTheme(
|
||||
'backgroundColor',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.tip}>
|
||||
使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色
|
||||
|
@ -519,7 +559,7 @@ const ConfigDialog: FC<{
|
|||
onChange={(e) => onNewSoundChange('name', e.target.value)}
|
||||
/>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到10k以下
|
||||
接口上传体积有限制,上传文件请全力压缩到10k以下,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -543,7 +583,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'图标素材'} required>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
|
||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
||||
</div>
|
||||
</InputContainer>
|
||||
{customTheme.icons.map((icon, idx) => (
|
||||
|
@ -668,7 +708,7 @@ const ConfigDialog: FC<{
|
|||
</div>
|
||||
)}
|
||||
<div className={style.tip}>
|
||||
接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||
接口上传内容总体积有限制,上传文件失败请尝试进一步压缩文件,推荐使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||
本地整活,勾选右侧关闭文件大小校验👉
|
||||
<input
|
||||
type={'checkbox'}
|
||||
|
@ -680,7 +720,12 @@ const ConfigDialog: FC<{
|
|||
(谨慎操作,单文件不超过1M为宜,文件过大可能导致崩溃,介时请刷新浏览器)
|
||||
</div>
|
||||
{configError && <div className={style.errorTip}>{configError}</div>}
|
||||
<WxQrCode />
|
||||
{customTheme.pure && (
|
||||
<div className={style.tip}>
|
||||
🎉🎉🎉恭喜发现彩蛋,生成的主题将开启纯净模式~
|
||||
</div>
|
||||
)}
|
||||
<WxQrCode onClick={() => setPureClickTime(pureClickTime + 1)} />
|
||||
<div className={'flex-container'}>
|
||||
<button
|
||||
className={'primary flex-grow'}
|
||||
|
@ -688,16 +733,17 @@ const ConfigDialog: FC<{
|
|||
>
|
||||
保存并预览
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
'primary flex-grow',
|
||||
style.uploadBtn,
|
||||
uploading && style.uploading
|
||||
)}
|
||||
onClick={onGenQrLinkClick}
|
||||
>
|
||||
生成二维码&链接
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* 'primary flex-grow',*/}
|
||||
{/* style.uploadBtn,*/}
|
||||
{/* uploading && style.uploading*/}
|
||||
{/* )}*/}
|
||||
{/* onClick={onGenQrLinkClick}*/}
|
||||
{/* disabled*/}
|
||||
{/*>*/}
|
||||
{/* 生成二维码&链接*/}
|
||||
{/*</button>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -171,6 +171,7 @@ const Game: FC<{
|
|||
Record<MySymbol['id'], number>
|
||||
>({});
|
||||
const [finished, setFinished] = useState<boolean>(false);
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [animating, setAnimating] = useState<boolean>(false);
|
||||
|
||||
// 音效
|
||||
|
@ -326,6 +327,7 @@ const Game: FC<{
|
|||
// 重开
|
||||
const restart = () => {
|
||||
setFinished(false);
|
||||
setSuccess(false);
|
||||
setScore(0);
|
||||
setLevel(1);
|
||||
setQueue([]);
|
||||
|
@ -395,21 +397,25 @@ const Game: FC<{
|
|||
// 输了
|
||||
if (updateQueue.length === 7) {
|
||||
setFinished(true);
|
||||
setSuccess(false);
|
||||
}
|
||||
|
||||
if (!updateScene.find((s) => s.status !== 2)) {
|
||||
// 胜利
|
||||
// 队列清空了
|
||||
if (level === maxLevel) {
|
||||
// 胜利
|
||||
setFinished(true);
|
||||
return;
|
||||
setSuccess(true);
|
||||
} else {
|
||||
// 升级
|
||||
// 通关奖励关卡对应数值分数
|
||||
setScore(score + level);
|
||||
setLevel(level + 1);
|
||||
setQueue([]);
|
||||
checkCover(makeScene(level + 1, theme.icons));
|
||||
}
|
||||
// 升级
|
||||
// 通关奖励关卡对应数值分数
|
||||
setScore(score + level);
|
||||
setLevel(level + 1);
|
||||
setQueue([]);
|
||||
checkCover(makeScene(level + 1, theme.icons));
|
||||
} else {
|
||||
// 更新队列
|
||||
setQueue(updateQueue);
|
||||
checkCover(updateScene);
|
||||
}
|
||||
|
@ -496,7 +502,8 @@ const Game: FC<{
|
|||
level={level}
|
||||
time={usedTime}
|
||||
score={score}
|
||||
success={level === maxLevel}
|
||||
success={success}
|
||||
pure={theme.pure}
|
||||
restartMethod={restart}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -37,8 +37,9 @@ const Score: FC<{
|
|||
score: number;
|
||||
time: number;
|
||||
success: boolean;
|
||||
pure?: boolean;
|
||||
restartMethod: () => void;
|
||||
}> = ({ level, score, time, success, restartMethod }) => {
|
||||
}> = ({ level, score, time, success, restartMethod, pure = false }) => {
|
||||
const [rankList, setRankList] = useState<RankInfo[]>([]);
|
||||
const [username, setUsername] = useState<string>(
|
||||
localStorage.getItem(USER_NAME_STORAGE_KEY) || ''
|
||||
|
@ -97,7 +98,10 @@ const Score: FC<{
|
|||
if (_userId) {
|
||||
setTimeout(() => {
|
||||
const rankEl = document.getElementById(_userId + 'el');
|
||||
rankEl?.scrollIntoView({ behavior: 'smooth' });
|
||||
rankEl?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
|
@ -219,7 +223,13 @@ const Score: FC<{
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div>{tip}</div>
|
||||
{tip && <div>{tip}</div>}
|
||||
|
||||
{__DIY__ && (
|
||||
<button className={'primary'} onClick={restartMethod}>
|
||||
再来一次
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!__DIY__ && (
|
||||
<div className={style.rankContainer}>
|
||||
|
@ -278,7 +288,7 @@ const Score: FC<{
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
<WxQrCode />
|
||||
{!pure && <WxQrCode />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
import React, { FC, MouseEventHandler, useState } from 'react';
|
||||
import style from './WxQrCode.module.scss';
|
||||
import classNames from 'classnames';
|
||||
const WxQrCode: FC<{ title?: string }> = ({
|
||||
title = '【广告位招租中】同时如果您喜欢这个项目的话,可以点击扫描下方收款码分摊后台相关费用,感谢~😘',
|
||||
const WxQrCode: FC<{ title?: string; onClick?: MouseEventHandler }> = ({
|
||||
title = '如果您喜欢这个项目的话,点击扫描下方收款码请我喝杯咖啡,感谢~😘',
|
||||
onClick,
|
||||
}) => {
|
||||
const [fullScreen, setFullScreen] = useState<Record<number, boolean>>({
|
||||
0: false,
|
||||
|
@ -28,7 +29,7 @@ const WxQrCode: FC<{ title?: string }> = ({
|
|||
});
|
||||
};
|
||||
return (
|
||||
<div className={style.wxQrCodeContainer}>
|
||||
<div className={style.wxQrCodeContainer} onClick={onClick}>
|
||||
<div className={style.wxQrCodeTitle}>{title}</div>
|
||||
{[1, 5, 8].map((num, idx) => (
|
||||
<div
|
||||
|
|
83
src/main.tsx
83
src/main.tsx
|
@ -5,11 +5,7 @@ import './styles/global.scss';
|
|||
import './styles/utils.scss';
|
||||
import Bmob from 'hydrogen-js-sdk';
|
||||
import {
|
||||
DEFAULT_BGM_STORAGE_KEY,
|
||||
domRelatedOptForTheme,
|
||||
LAST_LEVEL_STORAGE_KEY,
|
||||
LAST_SCORE_STORAGE_KEY,
|
||||
LAST_TIME_STORAGE_KEY,
|
||||
parsePathCustomThemeId,
|
||||
PLAYING_THEME_ID_STORAGE_KEY,
|
||||
resetScoreStorage,
|
||||
|
@ -77,58 +73,37 @@ Bmob.initialize(
|
|||
import.meta.env.VITE_BMOB_SECCODE
|
||||
);
|
||||
|
||||
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, increment } = res as any;
|
||||
localStorage.setItem(customThemeIdFromPath, content);
|
||||
try {
|
||||
const customTheme = JSON.parse(content);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
// 统计访问次数
|
||||
increment('visitNum');
|
||||
// @ts-ignore
|
||||
res.save();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
// 请求主题
|
||||
if (customThemeIdFromPath) {
|
||||
const storageTheme = localStorage.getItem(customThemeIdFromPath);
|
||||
if (storageTheme) {
|
||||
try {
|
||||
const customTheme = JSON.parse(storageTheme);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
} else {
|
||||
successTrans(getDefaultTheme());
|
||||
Bmob.Query('config')
|
||||
.get(customThemeIdFromPath)
|
||||
.then((res) => {
|
||||
const { content, increment } = res as any;
|
||||
localStorage.setItem(customThemeIdFromPath, content);
|
||||
try {
|
||||
const customTheme = JSON.parse(content);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
// 统计访问次数
|
||||
increment('visitNum');
|
||||
// @ts-ignore
|
||||
res.save();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 音效资源请求
|
||||
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(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
} else {
|
||||
loadTheme();
|
||||
successTrans(getDefaultTheme());
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import { Theme } from '../interface';
|
||||
import {
|
||||
DEFAULT_BGM_STORAGE_KEY,
|
||||
DEFAULT_CLICK_SOUND_STORAGE_KEY,
|
||||
DEFAULT_TRIPLE_SOUND_STORAGE_KEY,
|
||||
} from '../../utils';
|
||||
|
||||
const icons = <const>[
|
||||
`🎨`,
|
||||
|
@ -36,16 +31,13 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
|
|||
sounds: [
|
||||
{
|
||||
name: 'button-click',
|
||||
src:
|
||||
localStorage.getItem(DEFAULT_CLICK_SOUND_STORAGE_KEY) || '',
|
||||
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-button-click.mp3',
|
||||
},
|
||||
{
|
||||
name: 'triple',
|
||||
src:
|
||||
localStorage.getItem(DEFAULT_TRIPLE_SOUND_STORAGE_KEY) ||
|
||||
'',
|
||||
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-triple.mp3',
|
||||
},
|
||||
],
|
||||
bgm: localStorage.getItem(DEFAULT_BGM_STORAGE_KEY) || '',
|
||||
bgm: 'https://minio.streakingman.com/solvable-sheep-game/sound-disco.mp3',
|
||||
};
|
||||
};
|
||||
|
|
104
src/utils.ts
104
src/utils.ts
|
@ -9,9 +9,6 @@ export const LAST_CUSTOM_THEME_ID_STORAGE_KEY = 'lastCustomThemeId';
|
|||
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
|
||||
export const CUSTOM_THEME_STORAGE_KEY = 'customTheme';
|
||||
export const CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY = 'customThemeFileValidate';
|
||||
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 USER_NAME_STORAGE_KEY = 'username';
|
||||
export const USER_ID_STORAGE_KEY = 'userId';
|
||||
export const PLAYING_THEME_ID_STORAGE_KEY = 'playingThemeId';
|
||||
|
@ -174,3 +171,104 @@ export const timestampToUsedTimeString: (time: number) => string = (time) => {
|
|||
return '时间转换出错';
|
||||
}
|
||||
};
|
||||
|
||||
export const dataURLToFile: (dataURL: string, filename?: string) => File = (
|
||||
// #endregion dataURLToFile
|
||||
dataURL,
|
||||
filename
|
||||
) => {
|
||||
const isDataURL = dataURL.startsWith('data:');
|
||||
if (!isDataURL) throw new Error('Data URL 错误');
|
||||
const _fileName = filename || new Date().getTime().toString();
|
||||
const mimeType = dataURL.match(/^data:([^;]+);/)?.[1] || '';
|
||||
// base64转二进制
|
||||
const binaryString = atob(dataURL.split(',')[1]);
|
||||
let binaryStringLength = binaryString.length;
|
||||
const bytes = new Uint8Array(binaryStringLength);
|
||||
while (binaryStringLength--) {
|
||||
bytes[binaryStringLength] = binaryString.charCodeAt(binaryStringLength);
|
||||
}
|
||||
return new File([bytes], _fileName, { type: mimeType });
|
||||
};
|
||||
|
||||
interface drawImgSrcInCanvasParams {
|
||||
imgSrc: string;
|
||||
canvas: HTMLCanvasElement;
|
||||
scale?: number;
|
||||
}
|
||||
export const drawImgSrcInCanvas: (
|
||||
params: drawImgSrcInCanvasParams
|
||||
) => Promise<void> = async ({ imgSrc, canvas, scale = 1 }) => {
|
||||
if (scale < 0) throw new Error('scale不能小于0');
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx || !(ctx instanceof CanvasRenderingContext2D)) {
|
||||
throw new Error('Failed to get 2D context');
|
||||
}
|
||||
const img = document.createElement('img');
|
||||
img.setAttribute('src', imgSrc);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
const width = img.width * scale;
|
||||
const height = img.height * scale;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => {
|
||||
reject(new Error('图片加载失败'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
interface createCanvasByImgSrcParams {
|
||||
imgSrc: HTMLImageElement['src'];
|
||||
scale?: number;
|
||||
}
|
||||
export const createCanvasByImgSrc: (
|
||||
params: createCanvasByImgSrcParams
|
||||
) => Promise<HTMLCanvasElement> = async ({ imgSrc, scale = 1 }) => {
|
||||
if (scale < 0) throw new Error('scale不能小于0');
|
||||
const canvas = document.createElement('canvas');
|
||||
await drawImgSrcInCanvas({
|
||||
imgSrc,
|
||||
canvas,
|
||||
scale,
|
||||
});
|
||||
return canvas;
|
||||
};
|
||||
|
||||
interface CanvasToFileParams {
|
||||
canvas: HTMLCanvasElement;
|
||||
fileName?: string;
|
||||
maxFileSize?: number;
|
||||
}
|
||||
export const canvasToFile: (
|
||||
params: CanvasToFileParams
|
||||
) => Promise<File> = async ({ canvas, fileName, maxFileSize }) => {
|
||||
// #endregion canvasToFile
|
||||
const MIME_TYPE = 'image/png';
|
||||
const dataURL = canvas.toDataURL(MIME_TYPE);
|
||||
const _fileName = fileName || new Date().getTime().toString();
|
||||
const genFile = dataURLToFile(dataURL, _fileName);
|
||||
// 判断是否需要压缩
|
||||
if (maxFileSize && genFile.size > maxFileSize) {
|
||||
let scale = Math.sqrt(maxFileSize / genFile.size);
|
||||
if (scale > 0.9) scale = 0.9;
|
||||
// TODO 暂时通过canvas绘制缩放图像进行递归压缩,后续考虑其他方式
|
||||
// TODO 不断生成canvas, 调研内存是否会泄漏
|
||||
const _canvas = await createCanvasByImgSrc({
|
||||
imgSrc: dataURL,
|
||||
scale,
|
||||
});
|
||||
return canvasToFile({
|
||||
canvas: _canvas,
|
||||
fileName: _fileName,
|
||||
maxFileSize,
|
||||
});
|
||||
} else {
|
||||
return genFile;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user