chore: 更新素材资源和组件优化

- 更新陈云、鲁迅、聂耳等故居的展品图片素材
- 更新拼图细节图片
- 优化学习地图底图和详情图
- 更新音频素材
- 优化各故居 store 和页面组件
This commit is contained in:
zhongyidian committed 2026-08-12 11:35:09 +08:00
1 parent 165374b986
commit d445f81e60
159 files changed
+4238 -654

No files matched your search

@@ -62,7 +62,7 @@ const ContinueLearningModal = ({
</div>
{/* 正文 */}
<p className="mt-10 max-w-[720px] text-center text-4xl font-medium leading-relaxed tracking-wide text-[#5c3a1e]">
<p className="indent-18 mt-10 max-w-[720px] text-4xl font-medium leading-relaxed tracking-wide text-[#5c3a1e]">
{message}
</p>
+3 -1
View File
@@ -42,7 +42,9 @@ const ErrorAlert = ({
{/* 标题和副标题容器 - 带深色背景条 */}
<div className="w-full bg-black/70 px-20 py-20 flex flex-col items-center gap-4">
{/* 副标题 */}
<div className="text-white text-5xl font-medium tracking-widest leading-18">{text}</div>
<div className="text-white text-5xl font-medium tracking-widest leading-18 indent-26">
{text}
</div>
</div>
{/* 继续按钮 */}
@@ -136,7 +136,7 @@ const BasketQuizPanel = ({
renderOption={(option, isUsed) => (
<div
className={cn(
'relative min-h-[90px] px-10 flex items-center justify-center',
'relative min-h-[90px] px-10 flex items-center',
'rounded-[32px] transition-all duration-300',
'text-[28px] font-bold border-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)]',
isUsed || filledValues.has(option.value)
@@ -147,7 +147,7 @@ const BasketQuizPanel = ({
{!(isUsed || filledValues.has(option.value)) && (
<div className="absolute inset-0 rounded-[32px] bg-linear-to-br from-[#d4c5a9]/20 to-transparent pointer-events-none" />
)}
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)] text-center leading-snug">
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)] leading-snug">
{option.label}
</span>
</div>
@@ -155,7 +155,7 @@ const BasketQuizPanel = ({
renderDragging={(option) => (
<div
className={cn(
'relative min-h-[60px] px-10 flex items-center justify-center',
'relative min-h-[60px] px-10 flex items-center',
'rounded-[32px]',
'text-[20px] font-bold border-3 shadow-[0_16px_48px_rgba(240,190,117,0.6)]',
'border-[#f0be75] bg-linear-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3]',
@@ -163,7 +163,7 @@ const BasketQuizPanel = ({
)}
>
<div className="absolute inset-0 rounded-[32px] bg-linear-to-br from-[#d4c5a9]/20 to-transparent pointer-events-none" />
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)] text-center leading-snug whitespace-nowrap">
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)] leading-snug whitespace-nowrap">
{option.label}
</span>
</div>
+40 -18
View File
@@ -16,8 +16,15 @@ const DragQuizPanel = ({
blankSeparators,
value,
error,
feedbackType = 'error',
onChange,
onSubmit,
submitText = '提交',
disabled = false,
}: DragQuizPanelProps) => {
const isComplete = blanks.every((blank) =>
value.some((result) => result.blankId === blank.id && result.optionValue !== '')
)
// 跟踪之前的 visible 状态
const prevVisibleRef = useRef(visible)
@@ -62,7 +69,7 @@ const DragQuizPanel = ({
{filledValue ? (
<span className="text-[#f0be75] font-semibold flex items-center gap-3">
<span className="text-[28px]">{filledValue}</span>
<button
{/* <button
type="button"
className="w-7 h-7 flex items-center justify-center rounded-full bg-[#8b4513]/80 hover:bg-[#a0522d] text-white text-sm transition-all hover:scale-110 shadow-lg"
onClick={(e) => {
@@ -71,7 +78,7 @@ const DragQuizPanel = ({
}}
>
×
</button>
</button> */}
</span>
) : (
<span className="text-white/40 text-[24px] font-light">____</span>
@@ -87,48 +94,63 @@ const DragQuizPanel = ({
})}
{questionSuffix && <span>{questionSuffix}</span>}
{/* 答案反馈 */}
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
{error && (
<div
className={cn(
'mt-5 text-center text-[26px]',
feedbackType === 'success' ? 'text-[#b7f7d1]' : 'text-[#fcbe53]'
)}
>
{error}
</div>
)}
</div>
</div>
</div>
{/* 使用 DragFill 组件 */}
<div className="w-[1500px] mt-16">
{/* 选项区域 */}
<div className="w-full max-w-[1300px] mt-10">
<DragFill
options={options}
blanks={blanks}
value={value}
onChange={onChange}
disabled={disabled}
hideLabels
showSubmitButton={false}
layout="vertical"
className="w-full"
className="w-full h-auto!"
optionClassName="grid grid-cols-2 gap-5"
renderOption={(option, isUsed) => (
<div
className={cn(
'relative h-[110px] px-10 flex items-center justify-center',
'rounded-[32px] transition-all duration-300',
'text-[32px] font-bold border-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)]',
'px-10 py-4 flex items-center justify-center',
'rounded-2xl transition-all duration-300',
'text-[30px] font-bold border-2',
'shadow-[0_4px_16px_rgba(0,0,0,0.25)]',
isUsed
? 'border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50'
: 'border-[#d4c5a9] bg-linear-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3] cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)] hover:scale-[1.02]'
: 'border-[#d4c5a9] bg-linear-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3] cursor-move hover:border-[#f0be75] hover:shadow-[0_8px_28px_rgba(240,190,117,0.6)] hover:scale-[1.02]'
)}
>
{!isUsed && (
<div className="absolute inset-0 rounded-[32px] bg-linear-to-br from-[#d4c5a9]/20 to-transparent pointer-events-none" />
)}
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)]">
{option.label}
</span>
<span className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)]">{option.label}</span>
</div>
)}
renderBlank={(_blank) => {
// 返回一个隐藏元素,实际的空白区域在题干中
// 但需要通过 data-blank-id 让 DragFill 能找到它
return <div className="hidden" />
}}
/>
</div>
{/* 提交按钮 */}
<button
type="button"
disabled={disabled || !isComplete}
onClick={() => onSubmit?.(value)}
className="mt-8 px-24 py-3 text-[28px] font-medium text-white bg-[#c2995f] rounded-[40px] transition-all duration-200 hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
>
{submitText}
</button>
</div>
</div>
)
@@ -0,0 +1,208 @@
import { useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
import SuccessAlert from '@/components/layout/alert/success-alert'
import type { ErrorCorrectionQuizPanelProps } from './types'
/**
* ErrorCorrectionQuizPanel - 纠错题面板
* 横向展示多个文本区块,用户点击找出其中的错误
*/
const ErrorCorrectionQuizPanel = ({
visible,
zIndex = 10000,
title,
introText,
blocks,
errorCount,
onSubmit,
disabled = false,
}: ErrorCorrectionQuizPanelProps) => {
// 记录用户点击过的区块
const [clickedBlocks, setClickedBlocks] = useState<Set<number>>(new Set())
// 记录找到的错误数量
const [foundErrors, setFoundErrors] = useState(0)
// 当前显示在题干区域的反馈
const [feedbackBlockId, setFeedbackBlockId] = useState<number | null>(null)
// 临时高亮的错误选项
const [wrongFlashBlockId, setWrongFlashBlockId] = useState<number | null>(null)
const wrongFlashTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// 是否已完成
const [isCompleted, setIsCompleted] = useState(false)
// 重置状态
useEffect(() => {
if (visible) {
if (wrongFlashTimerRef.current) {
clearTimeout(wrongFlashTimerRef.current)
wrongFlashTimerRef.current = null
}
setClickedBlocks(new Set())
setFoundErrors(0)
setFeedbackBlockId(null)
setWrongFlashBlockId(null)
setIsCompleted(false)
}
return () => {
if (wrongFlashTimerRef.current) {
clearTimeout(wrongFlashTimerRef.current)
wrongFlashTimerRef.current = null
}
}
}, [visible])
// 处理区块点击
const handleBlockClick = (blockId: number) => {
if (disabled || isCompleted || clickedBlocks.has(blockId)) return
const block = blocks.find((b) => b.id === blockId)
if (!block) return
const newClickedBlocks = new Set(clickedBlocks)
setFeedbackBlockId(blockId)
if (wrongFlashTimerRef.current) {
clearTimeout(wrongFlashTimerRef.current)
wrongFlashTimerRef.current = null
}
setWrongFlashBlockId(null)
// 如果点击的是错误区块,增加找到的错误数
if (block.isError) {
newClickedBlocks.add(blockId)
setClickedBlocks(newClickedBlocks)
const newFoundErrors = foundErrors + 1
setFoundErrors(newFoundErrors)
// 检查是否找全了所有错误
if (newFoundErrors === errorCount) {
setIsCompleted(true)
}
} else {
setWrongFlashBlockId(blockId)
wrongFlashTimerRef.current = setTimeout(() => {
setWrongFlashBlockId(null)
wrongFlashTimerRef.current = null
}, 500)
}
}
// 获取区块的显示状态
const getBlockStatus = (blockId: number) => {
if (!clickedBlocks.has(blockId)) return 'unclicked'
const block = blocks.find((b) => b.id === blockId)
if (!block) return 'unclicked'
return block.isError ? 'found-error' : 'wrong-selection'
}
if (!visible) return null
const feedbackBlock = blocks.find((block) => block.id === feedbackBlockId)
const feedbackStatus = feedbackBlock?.isError ? 'found-error' : 'wrong-selection'
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/45" style={{ zIndex }}>
<div className="relative w-full max-w-[1728px] h-full max-h-[972px] flex flex-col items-center justify-center p-8">
{/* 题目区域 */}
<div className="w-full max-w-[1700px] rounded-[32px] bg-[#130f0b]/95 px-20 py-9 shadow-[0_20px_60px_rgba(240,190,117,0.75)] border-4 border-[#d4c5a9] mb-8">
<div className="flex items-start gap-6">
{/* <div className="shrink-0 mt-1 rounded-full border-2 border-[#fcbe53] bg-[#fcbe53]/15 px-5 py-2 text-[24px] font-bold text-[#fcbe53]">
纠错
</div> */}
<div className="flex-1">
<div className="text-[34px] leading-relaxed text-white">
{title}
<span className="ml-4 inline-flex rounded-full border border-[#fcbe53]/60 bg-[#fcbe53]/15 px-4 py-1 text-[24px] text-[#fcbe53] align-baseline">
已找到 {foundErrors} / {errorCount} 处错误
</span>
</div>
{introText && (
<div className="indent-13 mt-4 text-[26px] leading-relaxed text-white/75">
{introText}
</div>
)}
</div>
</div>
{feedbackBlock && (
<div
className={cn(
'mt-6 rounded-2xl border-2 px-8 py-4 text-center text-[26px] leading-relaxed',
feedbackStatus === 'found-error'
? 'border-[#fcbe53]/70 bg-[#fcbe53]/15 text-[#ffe3a8]'
: 'border-white/20 bg-white/10 text-white/85'
)}
>
{/* <span className="mr-3 font-bold">
{feedbackStatus === 'found-error' ? '找到疑点:' : '不是疑点:'}
</span> */}
{feedbackBlock.feedback}
</div>
)}
</div>
{/* 文本区块(横向排列) */}
<div className="w-full max-w-[1700px] grid grid-cols-5 gap-4">
{blocks.map((block) => {
const status = getBlockStatus(block.id)
const isClicked = clickedBlocks.has(block.id)
const isWrongFlashing = wrongFlashBlockId === block.id
return (
<div key={block.id} className="flex flex-col gap-3">
{/* 区块本身 */}
<div
className={cn(
'relative px-6 py-6 rounded-[26px] border-4 transition-all duration-300',
'text-[24px] leading-relaxed cursor-pointer select-none',
'min-h-[210px] flex items-center justify-center',
'shadow-[0_10px_26px_rgba(43,28,16,0.25)]',
!isClicked &&
!isWrongFlashing &&
'border-[#d4c5a9] bg-[#f5f0e6] text-[#5c3e2e] hover:border-[#fcbe53] hover:-translate-y-1 hover:shadow-[0_16px_32px_rgba(240,190,117,0.35)]',
isWrongFlashing &&
'border-white/70 bg-[#2c2925]/90 text-white shadow-[0_0_24px_rgba(255,255,255,0.2)]',
status === 'found-error' &&
'border-[#fcbe53] bg-linear-to-br from-[#5b321e] to-[#2a1810] text-[#fff3d8] shadow-[0_0_28px_rgba(252,190,83,0.55)]',
status === 'wrong-selection' &&
'border-white/70 bg-[#2c2925]/90 text-white shadow-[0_0_24px_rgba(255,255,255,0.2)]',
(disabled || isCompleted) && !isClicked && 'opacity-50 cursor-not-allowed'
)}
onClick={() => handleBlockClick(block.id)}
>
{isWrongFlashing && (
<div className="absolute -top-4 right-4 rounded-full border-2 border-white/60 bg-[#2c2925] px-4 py-2 text-[20px] font-bold text-white shadow-lg">
再看看
</div>
)}
{status === 'found-error' && (
<div className="absolute -top-4 right-4 rounded-full border-2 border-[#ffe3a8] bg-[#fcbe53] px-4 py-2 text-[20px] font-bold text-[#5c3218] shadow-lg">
已标记
</div>
)}
{status === 'wrong-selection' && (
<div className="absolute -top-4 right-4 rounded-full border-2 border-white/60 bg-[#2c2925] px-4 py-2 text-[20px] font-bold text-white shadow-lg">
再看看
</div>
)}
<div className="indent-12">{block.text}</div>
</div>
</div>
)
})}
</div>
{/* 完成提示 */}
<SuccessAlert
visible={isCompleted}
title="恭喜你,找出了所有错误!"
onClose={() => onSubmit?.(true)}
/>
</div>
</div>
)
}
export default ErrorCorrectionQuizPanel
@@ -0,0 +1,276 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import { cn } from '@/lib/utils'
import type { SortQuizPanelProps } from './types'
/**
* SortQuizPanel - 排序题面板
* 支持拖拽排序,用于让用户按正确顺序排列选项
*/
const SortQuizPanel = ({
visible,
zIndex = 10000,
title,
items,
correctOrder,
errorMessage,
successMessage,
onSubmit,
onError,
disabled = false,
submitText = '提交',
}: SortQuizPanelProps) => {
// 当前排序状态
const [currentOrder, setCurrentOrder] = useState<string[]>([])
const [error, setError] = useState<string>('')
const [isSubmitted, setIsSubmitted] = useState(false)
// 拖拽状态
const [draggingIndex, setDraggingIndex] = useState<number | null>(null)
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null)
const dragOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const panelRef = useRef<HTMLDivElement | null>(null)
// 初始化排序(使用items的初始顺序)
useEffect(() => {
if (visible && items.length > 0) {
setCurrentOrder(items.map((item) => item.id))
setError('')
setIsSubmitted(false)
}
}, [visible, items])
const getLocalPoint = useCallback((clientX: number, clientY: number) => {
const panelElement = panelRef.current
if (!panelElement) return { x: clientX, y: clientY }
const rect = panelElement.getBoundingClientRect()
const scaleX = rect.width / panelElement.offsetWidth || 1
const scaleY = rect.height / panelElement.offsetHeight || 1
return {
x: (clientX - rect.left) / scaleX,
y: (clientY - rect.top) / scaleY,
}
}, [])
const getLocalRect = useCallback((element: HTMLElement) => {
const panelElement = panelRef.current
if (!panelElement) return element.getBoundingClientRect()
const panelRect = panelElement.getBoundingClientRect()
const rect = element.getBoundingClientRect()
const scaleX = panelRect.width / panelElement.offsetWidth || 1
const scaleY = panelRect.height / panelElement.offsetHeight || 1
return {
left: (rect.left - panelRect.left) / scaleX,
top: (rect.top - panelRect.top) / scaleY,
width: rect.width / scaleX,
height: rect.height / scaleY,
right: (rect.right - panelRect.left) / scaleX,
bottom: (rect.bottom - panelRect.top) / scaleY,
}
}, [])
const updateDragOverIndex = useCallback((clientX: number, clientY: number) => {
for (let i = 0; i < itemRefs.current.length; i++) {
const element = itemRefs.current[i]
if (!element) continue
const rect = element.getBoundingClientRect()
if (
clientX >= rect.left &&
clientX <= rect.right &&
clientY >= rect.top &&
clientY <= rect.bottom
) {
setDragOverIndex(i)
return
}
}
setDragOverIndex(null)
}, [])
// 处理拖拽开始(Pointer Events 同时兼容鼠标、触摸和手写笔)
const handlePointerDown = (e: React.PointerEvent, index: number) => {
if (disabled || isSubmitted || e.button !== 0) return
const itemElement = itemRefs.current[index]
if (!itemElement) return
const localPoint = getLocalPoint(e.clientX, e.clientY)
const localRect = getLocalRect(itemElement)
dragOffsetRef.current = {
x: localPoint.x - localRect.left,
y: localPoint.y - localRect.top,
}
setDraggingIndex(index)
setDragPosition(localPoint)
updateDragOverIndex(e.clientX, e.clientY)
e.currentTarget.setPointerCapture(e.pointerId)
e.preventDefault()
}
// 处理放下
const handleDrop = useCallback(() => {
if (draggingIndex === null) return
if (dragOverIndex !== null && dragOverIndex !== draggingIndex) {
const newOrder = [...currentOrder]
const removed = newOrder[draggingIndex]
if (removed === undefined) return
newOrder.splice(draggingIndex, 1)
newOrder.splice(dragOverIndex, 0, removed)
setCurrentOrder(newOrder)
}
setDraggingIndex(null)
setDragOverIndex(null)
setDragPosition(null)
}, [currentOrder, dragOverIndex, draggingIndex])
// 处理拖拽移动
useEffect(() => {
if (draggingIndex === null) return
const handlePointerMove = (e: PointerEvent) => {
setDragPosition(getLocalPoint(e.clientX, e.clientY))
updateDragOverIndex(e.clientX, e.clientY)
e.preventDefault()
}
const handlePointerUp = () => {
handleDrop()
}
document.addEventListener('pointermove', handlePointerMove, { passive: false })
document.addEventListener('pointerup', handlePointerUp)
document.addEventListener('pointercancel', handlePointerUp)
return () => {
document.removeEventListener('pointermove', handlePointerMove)
document.removeEventListener('pointerup', handlePointerUp)
document.removeEventListener('pointercancel', handlePointerUp)
}
}, [draggingIndex, getLocalPoint, handleDrop, updateDragOverIndex])
// 提交验证
const handleSubmit = () => {
if (disabled || isSubmitted) return
const isCorrect = JSON.stringify(currentOrder) === JSON.stringify(correctOrder)
if (isCorrect) {
setError('')
setIsSubmitted(true)
onSubmit?.(currentOrder, true)
} else {
setError(errorMessage || '排序不正确,请重新调整顺序')
onError?.(errorMessage || '排序不正确,请重新调整顺序')
onSubmit?.(currentOrder, false)
}
}
if (!visible) return null
return (
<div
ref={panelRef}
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="relative w-full max-w-[1728px] h-full max-h-[972px] flex flex-col items-center justify-center p-8">
{/* 题目标题 */}
<div className="w-full max-w-[1700px] rounded-[32px] bg-black/90 px-20 py-8 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9] mb-8">
<div className="text-[34px] leading-relaxed text-white">{title}</div>
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
{isSubmitted && successMessage && (
<div className="mt-5 text-center text-[26px] text-[#4ade80]">{successMessage}</div>
)}
</div>
{/* 排序选项区域 */}
<div className="w-full max-w-[1500px] flex flex-col gap-4 mb-8">
{currentOrder.map((itemId, index) => {
const item = items.find((i) => i.id === itemId)
if (!item) return null
const isDragging = draggingIndex === index
const isDragOver = dragOverIndex === index && dragOverIndex !== draggingIndex
return (
<div
key={itemId}
ref={(el) => {
itemRefs.current[index] = el
}}
className={cn(
'relative px-8 py-6 rounded-2xl border-3 transition-all duration-200 touch-none',
'text-[28px] leading-relaxed select-none',
disabled || isSubmitted
? 'cursor-not-allowed opacity-60'
: 'cursor-grab active:cursor-grabbing',
isDragging
? 'opacity-50 scale-95'
: isDragOver
? 'border-[#f0be75] bg-[#f0be75]/20 scale-105'
: 'border-[#d4c5a9] bg-[#d7b888] text-[#5c3e2e] hover:border-[#f0be75] hover:shadow-lg'
)}
onPointerDown={(e) => handlePointerDown(e, index)}
>
<div className="flex items-center gap-4">
{/* 拖拽手柄图标 */}
<div className="flex flex-col gap-1 opacity-50">
<div className="w-6 h-1 bg-[#5c3e2e] rounded-full" />
<div className="w-6 h-1 bg-[#5c3e2e] rounded-full" />
<div className="w-6 h-1 bg-[#5c3e2e] rounded-full" />
</div>
<div className="flex-1">{item.label}</div>
</div>
</div>
)
})}
</div>
{/* 提交按钮 */}
{!isSubmitted && (
<button
type="button"
onClick={handleSubmit}
disabled={disabled}
className={cn(
'px-16 py-4 rounded-2xl text-[32px] font-bold transition-all duration-300',
'shadow-[0_8px_24px_rgba(0,0,0,0.3)]',
disabled
? 'bg-gray-400 text-gray-600 cursor-not-allowed'
: 'bg-[#f0be75] text-[#5c3e2e] hover:bg-[#e0ae65] hover:scale-105 hover:shadow-[0_12px_32px_rgba(240,190,117,0.6)]'
)}
>
{submitText}
</button>
)}
{/* 拖拽预览 */}
{draggingIndex !== null && dragPosition && (
<div
className="fixed w-full max-w-[1500px] pointer-events-none z-[10001]"
style={{
left: `${dragPosition.x - dragOffsetRef.current.x}px`,
top: `${dragPosition.y - dragOffsetRef.current.y}px`,
}}
>
<div className="px-8 py-6 rounded-2xl border-3 border-[#f0be75] bg-[#d7b888] text-[#5c3e2e] text-[28px] shadow-2xl opacity-90">
{items.find((i) => i.id === currentOrder[draggingIndex])?.label}
</div>
</div>
)}
</div>
</div>
)
}
export default SortQuizPanel
+6
View File
@@ -5,6 +5,8 @@ export { default as DragQuizPanel } from './DragQuizPanel'
export { default as BasketQuizPanel } from './BasketQuizPanel'
export { default as MatchQuizPanel } from './MatchQuizPanel'
export { default as SelectFillQuizPanel } from './SelectFillQuizPanel'
export { default as SortQuizPanel } from './SortQuizPanel'
export { default as ErrorCorrectionQuizPanel } from './ErrorCorrectionQuizPanel'
export type {
SingleChoiceQuizPanelProps,
MultipleChoiceQuizPanelProps,
@@ -15,6 +17,10 @@ export type {
SelectFillQuizPanelProps,
SelectFillBlank,
SelectFillResult,
SortQuizPanelProps,
SortQuizItem,
ErrorCorrectionQuizPanelProps,
ErrorCorrectionBlock,
} from './types'
// 保留旧组件以保持向后兼容
+72
View File
@@ -37,6 +37,12 @@ export interface DragQuizPanelProps {
feedbackType?: 'success' | 'error'
/** 值变化回调 */
onChange: (value: DragFillResult[]) => void
/** 所有空白填满后点击提交的回调 */
onSubmit?: (value: DragFillResult[]) => void
/** 提交按钮文本 */
submitText?: string
/** 是否禁用拖拽和提交 */
disabled?: boolean
}
/** 单选题面板配置 */
@@ -192,6 +198,72 @@ export interface SelectFillQuizPanelProps {
submitText?: string
}
/** 排序题项 */
export interface SortQuizItem {
/** 唯一标识 */
id: string
/** 显示文本 */
label: string
}
/** 排序题面板配置 */
export interface SortQuizPanelProps {
/** 是否可见 */
visible: boolean
/** z-index 层级 */
zIndex?: number
/** 题目 */
title: string
/** 待排序的选项列表(初始顺序) */
items: SortQuizItem[]
/** 正确的排序顺序(id数组) */
correctOrder: string[]
/** 错误提示信息 */
errorMessage?: string
/** 成功提示信息(可选) */
successMessage?: string
/** 提交回调,返回用户排序和是否正确 */
onSubmit?: (order: string[], isCorrect: boolean) => void
/** 错误回调(可选) */
onError?: (error: string) => void
/** 是否禁用 */
disabled?: boolean
/** 提交按钮文本 */
submitText?: string
}
/** 纠错题区块 */
export interface ErrorCorrectionBlock {
/** 区块ID */
id: number
/** 区块文本内容 */
text: string
/** 是否是错误 */
isError: boolean
/** 点击后的反馈文本 */
feedback: string
}
/** 纠错题面板配置 */
export interface ErrorCorrectionQuizPanelProps {
/** 是否可见 */
visible: boolean
/** z-index 层级 */
zIndex?: number
/** 题目 */
title: string
/** 情景介绍文本(可选) */
introText?: string
/** 文本区块列表 */
blocks: ErrorCorrectionBlock[]
/** 需要找出的错误数量 */
errorCount: number
/** 提交回调(找到所有错误时自动调用) */
onSubmit?: (isCorrect: boolean) => void
/** 是否禁用 */
disabled?: boolean
}
/** @deprecated 使用 SingleChoiceQuizPanelProps 替代 */
export interface QuizPanelProps {
visible: boolean
+6 -10
View File
@@ -237,7 +237,7 @@ const LetterTemplate = ({
} else if (/\d/.test(value)) {
showHint('不能包含数字')
} else if (/[\p{P}\p{S}]/u.test(value)) {
showHint('不能包含标点符号')
showHint('不能包含特殊符号')
}
}
@@ -331,7 +331,7 @@ const LetterTemplate = ({
} else if (/\d/.test(value)) {
showHint('不能包含数字')
} else if (/[\p{P}\p{S}]/u.test(value)) {
showHint('不能包含标点符号')
showHint('不能包含特殊符号')
}
}
@@ -549,14 +549,10 @@ const LetterTemplate = ({
/>
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:0px_0px_4px_#b37921,6px_6px_4px_#b37921,6px_6px_4px_#b37921,6px_6px_4px_#b37921]">
<div
className="text-white text-6xl font-bold tracking-[20px]"
style={{
writingMode: 'vertical-rl',
textOrientation: 'upright',
}}
>
{title}
<div className="flex flex-col items-center text-white text-6xl font-bold leading-none gap-[20px]">
{Array.from(title).map((char, index) => (
<span key={`title-${index}`}>{char}</span>
))}
</div>
</div>
+8 -3
View File
@@ -47,16 +47,21 @@ const fields: Record<number, LetterFieldConfig> = {
3: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[180px]',
placeholder: '结合所学专业',
widthClassName: 'w-[120px]',
readOnlyFallback: '专业',
},
4: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
widthClassName: 'w-[180px]',
placeholder: '结合所学专业',
readOnlyFallback: '实干报国',
},
5: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
},
}
const LetterToChenyun = ({
+44 -1
View File
@@ -575,8 +575,14 @@ const Video = forwardRef<VideoRef, VideoProps>(
// 计算进度百分比
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0
const disableFirstWatchVideoGating =
// 视频首次观看限制:env 默认值可被调试面板运行时覆盖
const envDisableGating =
import.meta.env.VITE_DISABLE_FIRST_WATCH_VIDEO_GATING === 'true'
const sessionOverride = window.sessionStorage.getItem('debug:video-gating')
const disableFirstWatchVideoGating =
sessionOverride !== null ? sessionOverride === 'disabled' : envDisableGating
const canShowSkipButton = !skipKey || disableFirstWatchVideoGating || isVideoCompleted
const canShowControls = !controlsOnReplay || disableFirstWatchVideoGating || isVideoCompleted
const shouldShowReadonlyProgress =
@@ -642,6 +648,43 @@ const Video = forwardRef<VideoRef, VideoProps>(
style={{ transform: `scaleX(${progressPercent / 100})` }}
/>
</div>
{/* 锚点标记:纵向竖标 + 圆点 */}
{visibleAnchors.map((anchor) => {
const anchorPercent = duration > 0 ? (anchor.time / duration) * 100 : 0
const isPassed = currentTime >= anchor.time
return (
<div
key={anchor.id}
className="absolute bottom-0 flex flex-col items-center"
style={{
left: `${anchorPercent}%`,
transform: 'translateX(-50%)',
}}
title={`题目: ${formatTime(anchor.time)}`}
>
{/* 竖线 */}
<div
className="w-px"
style={{
height: '14px',
background: isPassed
? 'linear-gradient(to top, #fbbf24, transparent)'
: 'linear-gradient(to top, rgba(255,255,255,0.7), transparent)',
}}
/>
{/* 圆点 */}
<div
className="w-2 h-2 rounded-full"
style={{
background: isPassed ? '#fbbf24' : '#ffffff',
boxShadow: isPassed
? '0 0 8px rgba(251,191,36,0.8)'
: '0 0 5px rgba(255,255,255,0.6)',
}}
/>
</div>
)
})}
</div>
)}
{/* 跳过按钮 */}