feat: add puzzle residence configuration and reflection progress logic
- Introduced `puzzleResidences.ts` to define puzzle residence configurations including properties like display name, position, and puzzle availability. - Added `reflectionProgress.ts` to manage badge unlocking logic based on place completion, including functions to determine unlocked badges and video sources for scenes.
This commit is contained in:
1 parent
e0fa47d21a
commit
f5f36f75b5
153 files changed
+3846
-2352
No files matched your search
@@ -5,6 +5,14 @@ import { filterInput } from '@/lib/utils/sensitiveWords'
|
||||
import { Image, Select, Video, type VideoRef } from '@/components'
|
||||
import { showHint } from '@/components/hint'
|
||||
|
||||
const formatCurrentDate = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = `${now.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${now.getDate()}`.padStart(2, '0')
|
||||
return `${year}.${month}.${day}`
|
||||
}
|
||||
|
||||
export type LetterFieldType = 'text' | 'input' | 'select'
|
||||
|
||||
export interface LetterFieldOption {
|
||||
@@ -47,6 +55,7 @@ export interface LetterTemplateProps {
|
||||
audioSrc?: string
|
||||
showVideo?: boolean
|
||||
videoSrc?: string
|
||||
replyVideoSrc?: string
|
||||
sceneImageSrc?: string
|
||||
replyButtonImageSrc?: string
|
||||
replyImageSrc?: string
|
||||
@@ -74,19 +83,26 @@ const LetterTemplate = ({
|
||||
audioSrc,
|
||||
showVideo = true,
|
||||
videoSrc = 'video/letter/neihua_jixin.mp4',
|
||||
replyVideoSrc,
|
||||
sceneImageSrc = 'components/letter/动画结束后场景',
|
||||
replyButtonImageSrc = 'components/letter/点击敬读',
|
||||
replyImageSrc,
|
||||
}: LetterTemplateProps) => {
|
||||
const [blankValues, setBlankValues] = useState<string[]>([])
|
||||
const [blankErrors, setBlankErrors] = useState<boolean[]>([])
|
||||
const [signatureName, setSignatureName] = useState('')
|
||||
const [signatureError, setSignatureError] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [showSendVideo, setShowSendVideo] = useState(false)
|
||||
const [showReplyVideo, setShowReplyVideo] = useState(false)
|
||||
const [showScene, setShowScene] = useState(false)
|
||||
const [showReplyImage, setShowReplyImage] = useState(false)
|
||||
const sendVideoRef = useRef<VideoRef>(null)
|
||||
const replyVideoRef = useRef<VideoRef>(null)
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
|
||||
const currentDate = useMemo(() => formatCurrentDate(), [])
|
||||
|
||||
const resolvedFields = useMemo(
|
||||
() => (typeof fields === 'function' ? fields(blankValues) : fields),
|
||||
[fields, blankValues]
|
||||
@@ -99,6 +115,13 @@ const LetterTemplate = ({
|
||||
(_, index) => blankValues[index] ?? ''
|
||||
)
|
||||
|
||||
const trimmedSignatureName = signatureName.trim()
|
||||
if (!trimmedSignatureName) {
|
||||
showHint('请填写落款姓名后再提交哦!')
|
||||
setSignatureError(true)
|
||||
return false
|
||||
}
|
||||
|
||||
const nextErrors = validate
|
||||
? validate(nextValues, letterContent)
|
||||
: nextValues.map((value, index) => {
|
||||
@@ -146,7 +169,10 @@ const LetterTemplate = ({
|
||||
setSubmitted(false)
|
||||
setBlankValues([])
|
||||
setBlankErrors([])
|
||||
setSignatureName('')
|
||||
setSignatureError(false)
|
||||
setShowSendVideo(false)
|
||||
setShowReplyVideo(false)
|
||||
setShowScene(false)
|
||||
setShowReplyImage(false)
|
||||
}
|
||||
@@ -157,14 +183,14 @@ const LetterTemplate = ({
|
||||
const audio = audioRef.current
|
||||
if (!audio || !audioSrc) return
|
||||
|
||||
if (visible && !showSendVideo) {
|
||||
if (visible && !showSendVideo && !showScene && !showReplyImage && !showReplyVideo) {
|
||||
audio.currentTime = 0
|
||||
audio.play().catch(() => {})
|
||||
} else {
|
||||
audio.pause()
|
||||
audio.currentTime = 0
|
||||
}
|
||||
}, [visible, showSendVideo, audioSrc])
|
||||
}, [visible, showSendVideo, showScene, showReplyImage, showReplyVideo, audioSrc])
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
@@ -252,6 +278,48 @@ const LetterTemplate = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleReplyVideoCanPlay = () => {
|
||||
if (showReplyVideo && replyVideoRef.current) {
|
||||
replyVideoRef.current.play().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignatureNameChange = (value: string) => {
|
||||
const nextValue = filterInput(value)
|
||||
setSignatureName(nextValue)
|
||||
|
||||
const hasError = !nextValue.trim()
|
||||
if (signatureError !== hasError) {
|
||||
setSignatureError(hasError)
|
||||
}
|
||||
}
|
||||
|
||||
const renderSignature = (readonly: boolean) => {
|
||||
if (readonly) {
|
||||
return (
|
||||
<div className="absolute right-[240px] bottom-[110px] z-99 flex flex-col items-end text-[26px] leading-[1.8] text-[#4e402e]">
|
||||
<span>{signatureName.trim() || ' '}</span>
|
||||
<span>{currentDate}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute right-[190px] bottom-[120px] z-99 flex flex-col items-end gap-2 text-[#4e402e]">
|
||||
<input
|
||||
name="signature-name"
|
||||
className={`h-[36px] bg-transparent border-b-2 border-dashed text-center outline-none w-[120px] ${signatureError ? 'border-red-500' : 'border-[#4e402e]'}`}
|
||||
style={{ fontSize: '26px' }}
|
||||
autoComplete="off"
|
||||
value={signatureName}
|
||||
maxLength={12}
|
||||
onChange={(e) => handleSignatureNameChange(e.target.value)}
|
||||
/>
|
||||
<span className="text-[26px] leading-[1.8]">{currentDate}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderLine = (readonly: boolean) => {
|
||||
let placeholderIndex = 0
|
||||
let bodyStarted = false
|
||||
@@ -354,7 +422,7 @@ const LetterTemplate = ({
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
onClick={onClose}
|
||||
// onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -416,6 +484,8 @@ const LetterTemplate = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderSignature(false)}
|
||||
|
||||
<div className="absolute bottom-[150px] -right-30">
|
||||
<img
|
||||
src={getStaticImage('components/letter/笔')}
|
||||
@@ -424,7 +494,7 @@ const LetterTemplate = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
|
||||
<div className="absolute bottom-[80px] left-62 z-99">
|
||||
<Image
|
||||
src={getStaticImage('components/letter/可提交状态')}
|
||||
alt="可提交状态"
|
||||
@@ -468,7 +538,9 @@ const LetterTemplate = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
|
||||
{renderSignature(true)}
|
||||
|
||||
<div className="absolute bottom-[80px] left-50 z-99">
|
||||
<Image
|
||||
src={getStaticImage('components/letter/寄出')}
|
||||
alt="寄出"
|
||||
@@ -577,12 +649,42 @@ const LetterTemplate = ({
|
||||
onClick={() => {
|
||||
setShowReplyImage(false)
|
||||
setShowScene(false)
|
||||
if (replyVideoSrc) {
|
||||
setShowReplyVideo(true)
|
||||
return
|
||||
}
|
||||
onClose?.()
|
||||
onAnimationEnd?.()
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{showReplyVideo && replyVideoSrc && (
|
||||
<motion.div
|
||||
key="reply-video"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="fixed inset-0 z-104 h-full w-full bg-black"
|
||||
>
|
||||
<Video
|
||||
ref={replyVideoRef}
|
||||
src={getPublicPath(replyVideoSrc)}
|
||||
className="h-full w-full"
|
||||
playsInline
|
||||
onCanPlay={handleReplyVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowReplyVideo(false)
|
||||
onClose?.()
|
||||
onAnimationEnd?.()
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
Reference in new issue
Block a user