mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-23 20:00:23 +08:00
refactor: 自定义主题表单布局重构
This commit is contained in:
parent
9f613ade82
commit
1890cc5a6f
|
@ -15,109 +15,146 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
color: rgb(255 255 255 / 87%);
|
color: rgb(255 255 255 / 87%);
|
||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
z-index: 20 !important;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
max-width: 500px;
|
||||||
|
bottom: 0;
|
||||||
|
animation: ease-in-out show 0.3s both;
|
||||||
|
transition: 0.3s;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap !important;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
margin: 36px 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%);
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
color: #213547;
|
color: #213547;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&Wrapper {
|
.errorTip {
|
||||||
z-index: 10;
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
top: 0;
|
|
||||||
width: calc(100% - 32px);
|
|
||||||
max-width: 500px;
|
|
||||||
bottom: 0;
|
|
||||||
animation: ease-in-out show 0.3s both;
|
|
||||||
transition: 0.3s;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap !important;
|
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
|
||||||
margin: 36px 0;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 19px 38px rgb(0 0 0 / 30%),
|
|
||||||
0 15px 12px rgb(0 0 0 / 22%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: crimson;
|
color: crimson;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.closeBtn {
|
||||||
margin: 8px 0;
|
position: sticky;
|
||||||
}
|
background-color: rgb(0 0 0 / 30%);
|
||||||
}
|
|
||||||
|
|
||||||
.add {
|
|
||||||
&Btn {
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 1px solid gray;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&Empty::before {
|
|
||||||
content: '+';
|
|
||||||
color: #999;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&Dialog {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
width: 90%;
|
|
||||||
padding: 12px;
|
|
||||||
transform: translateX(-50%) translateY(-60vh);
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.3s;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid #535bf2;
|
|
||||||
color: rgb(255 255 255 / 87%);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
// 写冗余了,待优化
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&Show {
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
&Btn {
|
|
||||||
flex-grow: 1;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
left: calc(100% - 36px);
|
||||||
|
top: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #f9f9f9;
|
backdrop-filter: blur(5px);
|
||||||
font-size: 1.5em;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
span {
|
svg {
|
||||||
transform: rotate(45deg);
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
word-break: keep-all;
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='file'] {
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
flex-grow: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color'] {
|
||||||
|
padding: 0 2px;
|
||||||
|
flex-grow: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
min-width: 74px;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.required .label {
|
||||||
|
&::after {
|
||||||
|
content: '*';
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.soundItem {
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
height: 30px;
|
||||||
|
min-width: 48px;
|
||||||
|
margin-left: 42px;
|
||||||
|
background-color: #888;
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-bottom-right-radius: 15px;
|
||||||
|
border-top-right-radius: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
height: 30px;
|
||||||
|
width: 100px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,240 +1,87 @@
|
||||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
import React, { FC, ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
import style from './ConfigDialog.module.scss';
|
import style from './ConfigDialog.module.scss';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Icon, Sound, Theme } from '../themes/interface';
|
import { Icon, Sound, Theme } from '../themes/interface';
|
||||||
import { QRCodeCanvas } from 'qrcode.react';
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
import Bmob from 'hydrogen-js-sdk';
|
import Bmob from 'hydrogen-js-sdk';
|
||||||
import { captureElement, LAST_UPLOAD_TIME_STORAGE_KEY } from '../utils';
|
import {
|
||||||
|
captureElement,
|
||||||
|
CUSTOM_THEME_STORAGE_KEY,
|
||||||
|
LAST_UPLOAD_TIME_STORAGE_KEY,
|
||||||
|
randomString,
|
||||||
|
wrapThemeDefaultSounds,
|
||||||
|
} from '../utils';
|
||||||
import { copy } from 'clipboard';
|
import { copy } from 'clipboard';
|
||||||
|
import { CloseIcon } from './CloseIcon';
|
||||||
|
import WxQrCode from './WxQrCode';
|
||||||
|
|
||||||
const STORAGEKEY = 'customTheme';
|
const InputContainer: FC<{
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}> = ({ label, children, required }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={style.divider} />
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'flex-container flex-center flex-no-wrap',
|
||||||
|
style.inputContainer,
|
||||||
|
required && style.required
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={style.label}>{label}</span>
|
||||||
|
<div className={'flex-container flex-column flex-grow'}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ConfigDialog: FC<{
|
const ConfigDialog: FC<{
|
||||||
closeMethod: () => void;
|
closeMethod: () => void;
|
||||||
previewMethod: (theme: Theme<string>) => void;
|
previewMethod: (theme: Theme<string>) => void;
|
||||||
}> = ({ closeMethod, previewMethod }) => {
|
}> = ({ closeMethod, previewMethod }) => {
|
||||||
const [sounds, setSounds] = useState<Sound[]>([]);
|
// 错误提示
|
||||||
const [icons, setIcons] = useState<Icon[]>([]);
|
|
||||||
const inputRefMap = useRef<
|
|
||||||
Record<
|
|
||||||
'name' | 'link' | 'clickSound' | 'tripleSound' | string,
|
|
||||||
HTMLInputElement | HTMLSelectElement
|
|
||||||
>
|
|
||||||
>({});
|
|
||||||
const [configError, setConfigError] = useState<string>('');
|
const [configError, setConfigError] = useState<string>('');
|
||||||
const [customThemeInfo, setCustomThemeInfo] = useState<{
|
// 生成链接
|
||||||
title: string;
|
|
||||||
desc?: string;
|
|
||||||
bgm?: string;
|
|
||||||
background?: string;
|
|
||||||
backgroundBlur?: boolean;
|
|
||||||
}>({ title: '', desc: '', bgm: '', background: '', backgroundBlur: false });
|
|
||||||
const [addDialog, setAddDialog] = useState<{
|
|
||||||
show: boolean;
|
|
||||||
type: 'sound' | 'icon';
|
|
||||||
iconForm?: Icon;
|
|
||||||
soundForm?: Sound;
|
|
||||||
error: string;
|
|
||||||
idx?: number;
|
|
||||||
}>({
|
|
||||||
show: false,
|
|
||||||
type: 'sound',
|
|
||||||
error: '',
|
|
||||||
});
|
|
||||||
const [genLink, setGenLink] = useState<string>('');
|
const [genLink, setGenLink] = useState<string>('');
|
||||||
const [pureCount, setPureCount] = useState<number>(0);
|
const [customTheme, setCustomTheme] = useState<Theme<any>>({
|
||||||
|
title: '',
|
||||||
|
sounds: [],
|
||||||
|
icons: new Array(10).fill(0).map(() => ({
|
||||||
|
name: randomString(4),
|
||||||
|
content: '',
|
||||||
|
clickSound: '',
|
||||||
|
tripleSound: '',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 编辑中音效
|
||||||
|
const [editSound, setEditSound] = useState<Sound>({ name: '', src: '' });
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let storageTheme: Theme<any> | undefined = undefined;
|
|
||||||
try {
|
try {
|
||||||
const configString = localStorage.getItem(STORAGEKEY);
|
const configString = localStorage.getItem(CUSTOM_THEME_STORAGE_KEY);
|
||||||
if (configString) {
|
if (configString) {
|
||||||
const parseRes = JSON.parse(configString);
|
const parseRes = JSON.parse(configString);
|
||||||
if (typeof parseRes === 'object') storageTheme = parseRes;
|
if (typeof parseRes === 'object') {
|
||||||
|
setCustomTheme(parseRes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
console.log(e);
|
||||||
}
|
}
|
||||||
if (!storageTheme) return;
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
desc = '',
|
|
||||||
bgm = '',
|
|
||||||
sounds,
|
|
||||||
icons,
|
|
||||||
background = '',
|
|
||||||
backgroundBlur = false,
|
|
||||||
} = storageTheme;
|
|
||||||
setSounds(
|
|
||||||
sounds.filter((s) => !['triple', 'button-click'].includes(s.name))
|
|
||||||
);
|
|
||||||
setIcons(
|
|
||||||
icons.map((icon) => {
|
|
||||||
if (icon.clickSound === 'button-click') icon.clickSound = '';
|
|
||||||
if (icon.tripleSound === 'triple') icon.tripleSound = '';
|
|
||||||
return icon;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setCustomThemeInfo({
|
|
||||||
title,
|
|
||||||
// @ts-ignore
|
|
||||||
desc,
|
|
||||||
bgm,
|
|
||||||
background,
|
|
||||||
backgroundBlur,
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 音效保存
|
|
||||||
const saveSound = (sound: Sound, idx?: number) => {
|
|
||||||
if (!sound.src.startsWith('https')) return '请输入https链接';
|
|
||||||
const newSounds = sounds.slice();
|
|
||||||
const newIcons = icons.slice();
|
|
||||||
if (idx != null) {
|
|
||||||
// 编辑
|
|
||||||
for (let i = 0; i < sounds.length; i++) {
|
|
||||||
if (sounds[i].name === sound.name && i !== idx) {
|
|
||||||
return '名称已存在';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 检查编辑的音效是否有引用并修改
|
|
||||||
const oldSoundName = sounds[idx].name;
|
|
||||||
for (const icon of newIcons) {
|
|
||||||
if (icon.clickSound === oldSoundName)
|
|
||||||
icon.clickSound = sound.name;
|
|
||||||
if (icon.tripleSound === oldSoundName)
|
|
||||||
icon.tripleSound = sound.name;
|
|
||||||
}
|
|
||||||
newSounds[idx] = sound;
|
|
||||||
} else {
|
|
||||||
// 新增
|
|
||||||
if (sounds.find((s) => s.name === sound.name)) return '名称已存在';
|
|
||||||
newSounds.push(sound);
|
|
||||||
}
|
|
||||||
setIcons(newIcons);
|
|
||||||
setSounds(newSounds);
|
|
||||||
};
|
|
||||||
const onSoundClick = (idx?: number) => {
|
|
||||||
if (addDialog.show) return;
|
|
||||||
setAddDialog({
|
|
||||||
idx,
|
|
||||||
show: true,
|
|
||||||
type: 'sound',
|
|
||||||
soundForm: {
|
|
||||||
name: '',
|
|
||||||
src: '',
|
|
||||||
},
|
|
||||||
error: '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 图片保存
|
|
||||||
const saveIcon = (icon: Icon, idx?: number) => {
|
|
||||||
if (
|
|
||||||
typeof icon.content !== 'string' ||
|
|
||||||
!icon.content?.startsWith('https')
|
|
||||||
)
|
|
||||||
return '请输入https链接';
|
|
||||||
const newIcons = icons.slice();
|
|
||||||
if (idx != null) {
|
|
||||||
// 编辑
|
|
||||||
for (let i = 0; i < icons.length; i++) {
|
|
||||||
if (icons[i].name === icon.name && i !== idx) {
|
|
||||||
return '名称已存在';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newIcons[idx] = icon;
|
|
||||||
} else {
|
|
||||||
// 新增
|
|
||||||
if (icons.find((i) => i.name === icon.name)) return '名称已存在';
|
|
||||||
newIcons.push(icon);
|
|
||||||
}
|
|
||||||
setIcons(newIcons);
|
|
||||||
};
|
|
||||||
const onIconClick = (idx?: number) => {
|
|
||||||
if (addDialog.show) return;
|
|
||||||
setAddDialog({
|
|
||||||
idx,
|
|
||||||
show: true,
|
|
||||||
type: 'icon',
|
|
||||||
iconForm:
|
|
||||||
idx != null
|
|
||||||
? { ...icons[idx] }
|
|
||||||
: {
|
|
||||||
name: '',
|
|
||||||
content: '',
|
|
||||||
tripleSound: '',
|
|
||||||
clickSound: '',
|
|
||||||
},
|
|
||||||
error: '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 回显
|
|
||||||
useEffect(() => {
|
|
||||||
const { show, type, idx } = addDialog;
|
|
||||||
if (show) return;
|
|
||||||
if (!inputRefMap.current) return;
|
|
||||||
if (type === 'icon') {
|
|
||||||
inputRefMap.current.name.value = idx != null ? icons[idx].name : '';
|
|
||||||
inputRefMap.current.link.value =
|
|
||||||
idx != null ? (icons[idx].content as string) : '';
|
|
||||||
inputRefMap.current.clickSound.value =
|
|
||||||
idx != null ? icons[idx]?.clickSound || '' : '';
|
|
||||||
inputRefMap.current.tripleSound.value =
|
|
||||||
idx != null ? icons[idx]?.tripleSound || '' : '';
|
|
||||||
} else {
|
|
||||||
inputRefMap.current.name.value =
|
|
||||||
idx != null ? sounds[idx].name : '';
|
|
||||||
inputRefMap.current.link.value = idx != null ? sounds[idx].src : '';
|
|
||||||
}
|
|
||||||
}, [addDialog]);
|
|
||||||
|
|
||||||
// 添加单项的点击
|
|
||||||
const onAddDialogSaveClick = () => {
|
|
||||||
const error = (addDialog.type === 'sound' ? saveSound : saveIcon)(
|
|
||||||
addDialog[`${addDialog.type}Form`] as any,
|
|
||||||
addDialog.idx
|
|
||||||
);
|
|
||||||
if (error) {
|
|
||||||
setAddDialog({ ...addDialog, error });
|
|
||||||
} else {
|
|
||||||
closeAddDialog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭添加弹窗
|
|
||||||
const closeAddDialog = () => {
|
|
||||||
setAddDialog({ ...addDialog, show: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成主题
|
// 生成主题
|
||||||
const generateTheme: () => Promise<Theme<any>> = async () => {
|
const generateTheme: () => Promise<Theme<any>> = async () => {
|
||||||
const { title, desc, bgm, background, backgroundBlur } =
|
// TODO 校验
|
||||||
customThemeInfo;
|
const cloneTheme = JSON.parse(JSON.stringify(customTheme));
|
||||||
if (bgm && !bgm.startsWith('https'))
|
wrapThemeDefaultSounds(cloneTheme);
|
||||||
return Promise.reject('背景音乐请输入https链接');
|
return Promise.resolve(cloneTheme);
|
||||||
if (background && !background.startsWith('https'))
|
|
||||||
return Promise.reject('背景图片请输入https链接');
|
|
||||||
if (!title) return Promise.reject('请填写标题');
|
|
||||||
if (icons.length !== 10) return Promise.reject('图片素材需要提供10张');
|
|
||||||
|
|
||||||
const customTheme: Theme<any> = {
|
|
||||||
// 恭喜你发现纯净模式彩蛋🎉,点击文字十次可以开启纯净模式
|
|
||||||
pure: pureCount !== 0 && pureCount % 10 === 0,
|
|
||||||
title,
|
|
||||||
desc,
|
|
||||||
bgm,
|
|
||||||
background,
|
|
||||||
backgroundBlur,
|
|
||||||
icons,
|
|
||||||
sounds,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(customTheme);
|
|
||||||
|
|
||||||
return Promise.resolve(JSON.parse(JSON.stringify(customTheme)));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 预览
|
// 预览
|
||||||
|
@ -243,7 +90,10 @@ const ConfigDialog: FC<{
|
||||||
generateTheme()
|
generateTheme()
|
||||||
.then((theme) => {
|
.then((theme) => {
|
||||||
previewMethod(theme);
|
previewMethod(theme);
|
||||||
localStorage.setItem(STORAGEKEY, JSON.stringify(theme));
|
localStorage.setItem(
|
||||||
|
CUSTOM_THEME_STORAGE_KEY,
|
||||||
|
JSON.stringify(theme)
|
||||||
|
);
|
||||||
closeMethod();
|
closeMethod();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -276,7 +126,7 @@ const ConfigDialog: FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringify = JSON.stringify(theme);
|
const stringify = JSON.stringify(theme);
|
||||||
localStorage.setItem(STORAGEKEY, stringify);
|
localStorage.setItem(CUSTOM_THEME_STORAGE_KEY, stringify);
|
||||||
const query = Bmob.Query('config');
|
const query = Bmob.Query('config');
|
||||||
query.set('content', stringify);
|
query.set('content', stringify);
|
||||||
query
|
query
|
||||||
|
@ -305,338 +155,101 @@ const ConfigDialog: FC<{
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除按钮
|
|
||||||
const DeleteBtn: FC<{ idx: number; type: 'sound' | 'icon' }> = ({
|
|
||||||
idx,
|
|
||||||
type,
|
|
||||||
}) => {
|
|
||||||
const deleteItem = () => {
|
|
||||||
if (type === 'sound') {
|
|
||||||
const newSounds = sounds.slice();
|
|
||||||
newSounds.splice(idx, 1);
|
|
||||||
setSounds(newSounds);
|
|
||||||
} else {
|
|
||||||
const newIcons = icons.slice();
|
|
||||||
newIcons.splice(idx, 1);
|
|
||||||
setIcons(newIcons);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className={style.deleteBtn} onClick={deleteItem}>
|
|
||||||
<span>+</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO HTML有点臭长了,待优化
|
// TODO HTML有点臭长了,待优化
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames(style.dialog)}>
|
||||||
className={classNames(
|
<div className={style.closeBtn} onClick={closeMethod}>
|
||||||
style.dialog,
|
<CloseIcon fill={'#fff'} />
|
||||||
style.dialogWrapper,
|
|
||||||
'flex-container flex-container'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<p onClick={() => setPureCount(pureCount + 1)}>
|
|
||||||
目前自定义仅支持配置https链接,可网上自行搜索素材复制链接,或者将自己处理好的素材上传第三方存储服务/图床上再复制外链
|
|
||||||
(想白嫖的话自行搜索【免费图床】【免费对象存储】【免费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>
|
</div>
|
||||||
|
<h2>自定义主题</h2>
|
||||||
|
|
||||||
{/*基本配置*/}
|
<InputContainer label={'标题'} required>
|
||||||
<h4 className="flex-container flex-center">
|
<input placeholder={'请输入标题'} />
|
||||||
标题:
|
</InputContainer>
|
||||||
<input
|
<InputContainer label={'描述'}>
|
||||||
value={customThemeInfo.title}
|
<input placeholder={'请输入描述'} />
|
||||||
placeholder="必填"
|
</InputContainer>
|
||||||
className="flex-grow"
|
<InputContainer label={'BGM'}>
|
||||||
onChange={(e) =>
|
<input type={'file'} />
|
||||||
setCustomThemeInfo({
|
<input placeholder={'或者输入https外链'} />
|
||||||
...customThemeInfo,
|
</InputContainer>
|
||||||
title: e.target.value,
|
<InputContainer label={'背景图'}>
|
||||||
})
|
<input type={'file'} />
|
||||||
}
|
<input placeholder={'或者输入https外链'} />
|
||||||
/>
|
<div className={'flex-container flex-center flex-no-wrap'}>
|
||||||
</h4>
|
<span>毛玻璃</span>
|
||||||
<h4 className="flex-container flex-center">
|
<input type={'checkbox'} />
|
||||||
描述:
|
<div className={'flex-spacer'} />
|
||||||
<input
|
<span>深色</span>
|
||||||
value={customThemeInfo.desc}
|
<input type={'checkbox'} />
|
||||||
placeholder="可选"
|
<div className={'flex-spacer'} />
|
||||||
className="flex-grow"
|
<span>纯色</span>
|
||||||
onChange={(e) =>
|
<input type={'color'} value="#fff" />
|
||||||
setCustomThemeInfo({
|
|
||||||
...customThemeInfo,
|
|
||||||
desc: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
<h4 className="flex-container flex-center">
|
|
||||||
背景音乐:
|
|
||||||
<input
|
|
||||||
value={customThemeInfo.bgm}
|
|
||||||
placeholder="可选 https://example.com/src.audio"
|
|
||||||
className="flex-grow"
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomThemeInfo({
|
|
||||||
...customThemeInfo,
|
|
||||||
bgm: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
<h4 className="flex-container flex-center">
|
|
||||||
背景图片:
|
|
||||||
<input
|
|
||||||
value={customThemeInfo.background}
|
|
||||||
placeholder="可选 https://example.com/src.image"
|
|
||||||
className="flex-grow"
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomThemeInfo({
|
|
||||||
...customThemeInfo,
|
|
||||||
background: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{customThemeInfo?.background?.startsWith('https') && (
|
|
||||||
<>
|
|
||||||
毛玻璃:
|
|
||||||
<input
|
|
||||||
checked={customThemeInfo.backgroundBlur}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomThemeInfo({
|
|
||||||
...customThemeInfo,
|
|
||||||
backgroundBlur: e.target.checked,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<h4>音效素材</h4>
|
|
||||||
<div className="flex-container">
|
|
||||||
{sounds.map((sound, idx) => (
|
|
||||||
<div
|
|
||||||
className="flex-container flex-column"
|
|
||||||
key={sound.name}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={() => onSoundClick(idx)}
|
|
||||||
className={classNames(style.addBtn)}
|
|
||||||
>
|
|
||||||
{sound.name}
|
|
||||||
</div>
|
|
||||||
<DeleteBtn idx={idx} type={'sound'} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{sounds.length < 20 && (
|
|
||||||
<div
|
|
||||||
onClick={() => onSoundClick()}
|
|
||||||
className={classNames(style.addBtn, style.addBtnEmpty)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h4>图片素材 {icons.length}/10 </h4>
|
|
||||||
<div className="flex-container">
|
|
||||||
{icons.map((icon, idx) => (
|
|
||||||
<div className="flex-container flex-column" key={icon.name}>
|
|
||||||
<div
|
|
||||||
onClick={() => onIconClick(idx)}
|
|
||||||
className={classNames(style.addBtn)}
|
|
||||||
>
|
|
||||||
{/* @ts-ignore*/}
|
|
||||||
<img src={icon.content} alt="" />
|
|
||||||
</div>
|
|
||||||
<DeleteBtn idx={idx} type={'icon'} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{icons.length < 10 && (
|
|
||||||
<div
|
|
||||||
onClick={() => onIconClick()}
|
|
||||||
className={classNames(style.addBtn, style.addBtnEmpty)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-spacer" />
|
|
||||||
{genLink && (
|
|
||||||
<div className="flex-container flex-column">
|
|
||||||
<QRCodeCanvas id="qrCode" value={genLink} size={300} />
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
captureElement('qrCode', customThemeInfo.title)
|
|
||||||
}
|
|
||||||
className="primary"
|
|
||||||
>
|
|
||||||
下载二维码
|
|
||||||
</button>
|
|
||||||
<div>{genLink}</div>
|
|
||||||
<button onClick={() => copy(genLink)} className="primary">
|
|
||||||
复制链接
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className={style.tip}>
|
||||||
{configError && <div className={style.error}>{configError}</div>}
|
使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色
|
||||||
<div className={style.error}>
|
</div>
|
||||||
由于访问量过大,生成二维码码和链接功能暂时关闭(个人项目经费有限且本项目没有接任何广告,感谢理解😭,但仍然可以本地整活,之前生成的二维码和链接依然有效),
|
</InputContainer>
|
||||||
本项目开源,有需求可以自行fork进行部署:
|
<InputContainer label={'关卡数'}>
|
||||||
<a
|
<input
|
||||||
href="https://github.com/StreakingMan/solvable-sheep-game"
|
type={'number'}
|
||||||
target="_blank"
|
placeholder={'最低5关,最高...理论上无限,默认为50'}
|
||||||
rel="noreferrer"
|
/>
|
||||||
style={{ textDecoration: 'underline' }}
|
</InputContainer>
|
||||||
|
<InputContainer label={'音效素材'} required>
|
||||||
|
<div className={'flex-container flex-left-center'}>
|
||||||
|
{customTheme.sounds.map((sound, idx) => {
|
||||||
|
return (
|
||||||
|
<div key={sound.name} className={style.soundItem}>
|
||||||
|
<audio src={sound.src} controls />
|
||||||
|
<div className={style.inner}>
|
||||||
|
<span>{sound.name}</span>
|
||||||
|
<CloseIcon fill={'#fff'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
placeholder={'输入音效名称'}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditSound({
|
||||||
|
name: event.target.value,
|
||||||
|
src: editSound.src,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input type={'file'} />
|
||||||
|
<input
|
||||||
|
placeholder={'或者输入https外链'}
|
||||||
|
onChange={(event) =>
|
||||||
|
setEditSound({
|
||||||
|
src: event.target.value,
|
||||||
|
name: editSound.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setCustomTheme({
|
||||||
|
...customTheme,
|
||||||
|
sounds: [...customTheme.sounds, editSound],
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
github仓库地址
|
添加音效
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex-container">
|
|
||||||
<button className="flex-grow" onClick={onPreviewClick}>
|
|
||||||
保存并预览
|
|
||||||
</button>
|
</button>
|
||||||
{/*<button className="flex-grow" onClick={onGenQrLinkClick}>
|
</InputContainer>
|
||||||
生成二维码&链接{uploading && '...'}
|
<InputContainer label={'图标素材'} required>
|
||||||
</button>*/}
|
<div className={'flex-container flex-left-center'}>
|
||||||
<button className="flex-grow" onClick={closeMethod}>
|
{customTheme.icons.map((icon, idx) => {
|
||||||
关闭
|
return <div key={icon.name}>{icon.name}</div>;
|
||||||
</button>
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*添加弹窗*/}
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
style.addDialog,
|
|
||||||
addDialog.show && style.addDialogShow,
|
|
||||||
'flex-container flex-column'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex-container flex-center">
|
|
||||||
名称:
|
|
||||||
<input
|
|
||||||
ref={(ref) => ref && (inputRefMap.current.name = ref)}
|
|
||||||
className="flex-grow"
|
|
||||||
placeholder="唯一名称"
|
|
||||||
onChange={(e) =>
|
|
||||||
setAddDialog({
|
|
||||||
...addDialog,
|
|
||||||
[`${addDialog.type}Form`]: {
|
|
||||||
...addDialog[`${addDialog.type}Form`],
|
|
||||||
name: e.target.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-container flex-center">
|
</InputContainer>
|
||||||
链接:
|
<InputContainer label={'操作音效'}>??</InputContainer>
|
||||||
<input
|
<WxQrCode />
|
||||||
ref={(ref) => ref && (inputRefMap.current.link = ref)}
|
|
||||||
className="flex-grow"
|
|
||||||
placeholder="https://example.com/src.audioOrImage"
|
|
||||||
onChange={(e) =>
|
|
||||||
setAddDialog({
|
|
||||||
...addDialog,
|
|
||||||
[`${addDialog.type}Form`]: {
|
|
||||||
...addDialog[`${addDialog.type}Form`],
|
|
||||||
[addDialog.type === 'sound'
|
|
||||||
? 'src'
|
|
||||||
: 'content']: e.target.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{addDialog.type === 'icon' && (
|
|
||||||
<>
|
|
||||||
<div className="flex-container flex-center">
|
|
||||||
点击音效:
|
|
||||||
<select
|
|
||||||
ref={(ref) =>
|
|
||||||
ref &&
|
|
||||||
(inputRefMap.current.clickSound = ref)
|
|
||||||
}
|
|
||||||
className="flex-grow"
|
|
||||||
onChange={(e) =>
|
|
||||||
setAddDialog({
|
|
||||||
...addDialog,
|
|
||||||
/*@ts-ignore*/
|
|
||||||
iconForm: {
|
|
||||||
...addDialog.iconForm,
|
|
||||||
clickSound: e.target.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="">默认</option>
|
|
||||||
{sounds.map((s) => (
|
|
||||||
<option key={s.name} value={s.name}>
|
|
||||||
{s.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex-container flex-center">
|
|
||||||
三连音效:
|
|
||||||
<select
|
|
||||||
ref={(ref) =>
|
|
||||||
ref &&
|
|
||||||
(inputRefMap.current.tripleSound = ref)
|
|
||||||
}
|
|
||||||
className="flex-grow"
|
|
||||||
onChange={(e) =>
|
|
||||||
setAddDialog({
|
|
||||||
...addDialog,
|
|
||||||
/*@ts-ignore*/
|
|
||||||
iconForm: {
|
|
||||||
...addDialog.iconForm,
|
|
||||||
tripleSound: e.target.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="">默认</option>
|
|
||||||
{sounds.map((s) => (
|
|
||||||
<option key={s.name} value={s.name}>
|
|
||||||
{s.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{addDialog.error && (
|
|
||||||
<div className={style.error}>{addDialog.error}</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-container">
|
|
||||||
<button className="flex-grow" onClick={closeAddDialog}>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="flex-grow primary"
|
|
||||||
onClick={onAddDialogSaveClick}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,6 +37,16 @@
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
|
|
|
@ -30,16 +30,13 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
select {
|
select {
|
||||||
border: 1px solid gray;
|
border: 1px dashed rgb(0 0 0 / 30%);
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: 1px solid gray;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-left-center {
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user