import React, { FC, ReactNode, useEffect, useState } from 'react'; import style from './ConfigDialog.module.scss'; import classNames from 'classnames'; import { Icon, Sound, Theme } from '../themes/interface'; import { QRCodeCanvas } from 'qrcode.react'; import Bmob from 'hydrogen-js-sdk'; import { captureElement, CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY, LAST_CUSTOM_THEME_ID_STORAGE_KEY, CUSTOM_THEME_STORAGE_KEY, deleteThemeUnusedSounds, getFileBase64String, linkReg, randomString, wrapThemeDefaultSounds, LAST_UPLOAD_TIME_STORAGE_KEY, } from '../utils'; import { copy } from 'clipboard'; import { CloseIcon } from './CloseIcon'; import WxQrCode from './WxQrCode'; const InputContainer: FC<{ label: string; required?: boolean; children?: ReactNode; }> = ({ label, children, required }) => { return ( <>
{label}
{children}
); }; interface CustomIcon extends Icon { content: string; } interface CustomTheme extends Theme { icons: CustomIcon[]; } const ConfigDialog: FC<{ closeMethod: () => void; previewMethod: (theme: Theme) => void; }> = ({ closeMethod, previewMethod }) => { // 错误提示 const [configError, setConfigError] = useState(''); // 生成链接 const [genLink, setGenLink] = useState(''); // 主题大对象 const [customTheme, setCustomTheme] = useState({ title: '', sounds: [], pure: false, icons: new Array(10).fill(0).map(() => ({ name: randomString(4), content: '', clickSound: '', tripleSound: '', })), }); function updateCustomTheme(key: keyof CustomTheme, value: any) { if (['sounds', 'icons'].includes(key)) { if (Array.isArray(value)) { setCustomTheme({ ...customTheme, [key]: [...value], }); } else { setCustomTheme({ ...customTheme, [key]: [...customTheme[key as 'sounds' | 'icons'], value], }); } } else { setCustomTheme({ ...customTheme, [key]: value, }); } } useEffect(() => { console.log(customTheme); }, [customTheme]); // 音效 const [newSound, setNewSound] = useState({ name: '', src: '' }); const [soundError, setSoundError] = useState(''); const onNewSoundChange = (key: keyof Sound, value: string) => { setNewSound({ ...newSound, [key]: value, }); }; const onAddNewSoundClick = () => { setSoundError(''); let error = ''; if (!linkReg.test(newSound.src)) error = '请输入https链接'; if (!newSound.name) error = '请输入音效名称'; if (customTheme.sounds.find((s) => s.name === newSound.name)) error = '名称已存在'; if (error) { setSoundError(error); } else { updateCustomTheme('sounds', newSound); setNewSound({ name: '', src: '' }); } }; const onDeleteSoundClick = (idx: number) => { const deleteSoundName = customTheme.sounds[idx].name; const findIconUseIdx = customTheme.icons.findIndex( ({ clickSound, tripleSound }) => [clickSound, tripleSound].includes(deleteSoundName) ); if (findIconUseIdx !== -1) { return setSoundError( `第${findIconUseIdx + 1}项图标有使用该音效,请取消后再删除` ); } const newSounds = customTheme.sounds.slice(); newSounds.splice(idx, 1); updateCustomTheme('sounds', newSounds); }; // 本地文件选择 const [bgmError, setBgmError] = useState(''); const [backgroundError, setBackgroundError] = useState(''); const [iconErrors, setIconErrors] = useState( new Array(10).fill('') ); // 文件体积校验开关 const [enableFileSizeValidate, setEnableFileSizeValidate] = useState( localStorage.getItem(CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY) !== 'false' ); useEffect(() => { localStorage.setItem( CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY, enableFileSizeValidate + '' ); }, [enableFileSizeValidate]); const makeIconErrors = (idx: number, error: string) => new Array(10) .fill('') .map((item, _idx) => (idx === _idx ? error : iconErrors[_idx])); const onFileChange: (props: { type: 'bgm' | 'background' | 'sound' | 'icon'; file?: File; idx?: number; }) => void = ({ type, file, idx }) => { if (!file) return; switch (type) { case 'bgm': setBgmError(''); if (enableFileSizeValidate && file.size > 80 * 1024) { return setBgmError('请选择80k以内全损音质的文件'); } getFileBase64String(file) .then((res) => { updateCustomTheme('bgm', res); }) .catch((e) => { setBgmError(e); }); break; case 'background': setBackgroundError(''); if (enableFileSizeValidate && file.size > 80 * 1024) { return setBackgroundError('请选择80k以内全损画质的图片'); } getFileBase64String(file) .then((res) => { updateCustomTheme('background', res); }) .catch((e) => { setBackgroundError(e); }); break; case 'sound': setSoundError(''); if (enableFileSizeValidate && file.size > 10 * 1024) { return setSoundError('请选择10k以内的音频文件'); } getFileBase64String(file) .then((res) => { onNewSoundChange('src', res); }) .catch((e) => { setSoundError(e); }); break; case 'icon': if (idx == null) return; setIconErrors(makeIconErrors(idx, '')); if (enableFileSizeValidate && file.size > 5 * 1024) { return setIconErrors( makeIconErrors(idx, '请选择5k以内的图片文件') ); } getFileBase64String(file) .then((res) => { updateCustomTheme( 'icons', customTheme.icons.map((icon, _idx) => _idx === idx ? { ...icon, content: res } : icon ) ); }) .catch((e) => { setIconErrors(makeIconErrors(idx, e)); }); break; } }; // 图标更新 const updateIcons = (key: keyof CustomIcon, value: string, idx: number) => { const newIcons = customTheme.icons.map((icon, _idx) => _idx === idx ? { ...icon, [key]: value, } : icon ); updateCustomTheme('icons', newIcons); }; // 初始化 useEffect(() => { const lastId = localStorage.getItem(LAST_CUSTOM_THEME_ID_STORAGE_KEY); lastId && setGenLink(`${location.origin}?customTheme=${lastId}`); try { const configString = localStorage.getItem(CUSTOM_THEME_STORAGE_KEY); if (configString) { const parseRes = JSON.parse(configString); if (typeof parseRes === 'object') { setCustomTheme(parseRes); } } } catch (e) { console.log(e); } }, []); // 校验主题 const validateTheme: () => Promise = async () => { // 校验 if (!customTheme.title) return Promise.reject('请输入标题'); if (customTheme.bgm && !linkReg.test(customTheme.bgm)) return Promise.reject('bgm请输入https链接'); if (customTheme.background && !linkReg.test(customTheme.background)) return Promise.reject('背景图请输入https链接'); if (!customTheme.maxLevel || customTheme.maxLevel < 5) return Promise.reject('请输入大于5的关卡数'); const findIconError = iconErrors.find((i) => !!i); if (findIconError) return Promise.reject(`图标素材有错误:${findIconError}`); const findUnfinishedIconIdx = customTheme.icons.findIndex( (icon) => !icon.content ); if (findUnfinishedIconIdx !== -1) { setIconErrors(makeIconErrors(findUnfinishedIconIdx, '请填写链接')); return Promise.reject( `第${findUnfinishedIconIdx + 1}图标素材未完成` ); } return Promise.resolve(''); }; // 预览 const onPreviewClick = () => { setConfigError(''); validateTheme() .then(() => { const cloneTheme = JSON.parse(JSON.stringify(customTheme)); wrapThemeDefaultSounds(cloneTheme); previewMethod(cloneTheme); localStorage.setItem( CUSTOM_THEME_STORAGE_KEY, JSON.stringify(customTheme) ); closeMethod(); }) .catch((e) => { setConfigError(e); }); }; // 生成二维码和链接 const [uploading, setUploading] = useState(false); const onGenQrLinkClick = () => { if (uploading) return; if (!enableFileSizeValidate) return setConfigError('请先开启文件大小校验'); let passTime = Number.MAX_SAFE_INTEGER; const lastUploadTime = localStorage.getItem( LAST_UPLOAD_TIME_STORAGE_KEY ); if (lastUploadTime) { passTime = Date.now() - Number(lastUploadTime); } if (passTime < 1000 * 60 * 15) { return setConfigError( `为节省请求数,15分钟内只能生成一次二维码,还剩大约${ 15 - Math.round(passTime / 1000 / 60) }分钟,先本地预览调整下吧~` ); } setUploading(true); setConfigError(''); validateTheme() .then(() => { const cloneTheme = JSON.parse(JSON.stringify(customTheme)); deleteThemeUnusedSounds(cloneTheme); const stringify = JSON.stringify(cloneTheme); localStorage.setItem(CUSTOM_THEME_STORAGE_KEY, stringify); const query = Bmob.Query('config'); query.set('content', stringify); query .save() .then((res) => { localStorage.setItem( LAST_CUSTOM_THEME_ID_STORAGE_KEY, //@ts-ignore res.objectId ); localStorage.setItem( LAST_UPLOAD_TIME_STORAGE_KEY, Date.now().toString() ); setTimeout(() => { setGenLink( `${location.origin}?customTheme=${ /*@ts-ignore*/ res.objectId || id }` ); }, 3000); }) .catch(({ error, code }) => { setTimeout(() => { setConfigError( code === 10007 ? '上传数据长度已超过bmob的限制' : error ); }, 3000); }) .finally(() => { setTimeout(() => { setUploading(false); }, 3000); }); }) .catch((e) => { setConfigError(e); setUploading(false); }); }; // 彩蛋 const [pureClickTime, setPureClickTime] = useState(0); useEffect(() => { updateCustomTheme( 'pure', pureClickTime % 5 === 0 && pureClickTime !== 0 ); }, [pureClickTime]); return (

自定义主题

updateCustomTheme('title', e.target.value)} /> updateCustomTheme('desc', e.target.value)} />
接口上传体积有限制,上传文件请全力压缩到80k以下
onFileChange({ type: 'bgm', file: e.target.files?.[0], }) } /> {bgmError &&
{bgmError}
} updateCustomTheme('bgm', e.target.value)} /> {customTheme.bgm &&
接口上传体积有限制,上传文件请全力压缩到80k以下
onFileChange({ type: 'background', file: e.target.files?.[0], }) } /> {backgroundError && (
{backgroundError}
)}
updateCustomTheme('background', e.target.value) } /> {customTheme.background && ( 加载失败 )}
毛玻璃 updateCustomTheme( 'backgroundBlur', e.target.checked ) } />
深色 updateCustomTheme('dark', e.target.checked) } />
纯色 updateCustomTheme( 'backgroundColor', e.target.value ) } />
使用图片或者纯色作为背景,图片可开启毛玻璃效果。如果你使用了深色的图片和颜色,请开启深色模式,此时标题等文字将变为亮色
updateCustomTheme('maxLevel', Number(e.target.value)) } />
{customTheme.sounds.map((sound, idx) => { return (
); })}
onNewSoundChange('name', e.target.value)} />
接口上传体积有限制,上传文件请全力压缩到10k以下
onFileChange({ type: 'sound', file: e.target.files?.[0], }) } /> onNewSoundChange('src', e.target.value)} /> {soundError && (
{soundError}
)}
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
{customTheme.icons.map((icon, idx) => (
onFileChange({ type: 'icon', file: e.target.files?.[0], idx, }) } />
{ setIconErrors( makeIconErrors( idx, linkReg.test(e.target.value) ? '' : '请输入https外链' ) ); }} onChange={(e) => updateIcons('content', e.target.value, idx) } /> {iconErrors[idx] && (
{iconErrors[idx]}
)}
))} {/*??*/} {genLink && (
{genLink}
)}
接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。 本地整活,勾选右侧关闭文件大小校验👉 setEnableFileSizeValidate(!e.target.checked) } /> (谨慎操作,单文件不超过1M为宜,文件过大可能导致崩溃,介时请刷新浏览器)
{configError &&
{configError}
} {customTheme.pure && (
🎉🎉🎉恭喜发现彩蛋,生成的主题将开启纯净模式~
)} setPureClickTime(pureClickTime + 1)} />
); }; export default ConfigDialog;