更新依赖项及历史人物相关内容资源
- 更新 package.json 和 package-lock.json 中的依赖项 - 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件 - 新增场景和组件的图片资源 - 清理未使用的音频和图片文件 - 优化多个组件和页面的实现逻辑
This commit is contained in:
1 parent
b1d55b9464
commit
e0fa47d21a
629 files changed
+24390
-2991
No files matched your search
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { DragFill } from '@/components'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { DragQuizPanelProps } from './types'
|
||||
|
||||
/**
|
||||
* BasketQuizPanel - 篮子拖拽题面板
|
||||
*
|
||||
* 左右布局:左边篮子放置区,右边选项列表(每个选项单独一行)
|
||||
*/
|
||||
const BasketQuizPanel = ({
|
||||
visible,
|
||||
zIndex = 10000,
|
||||
title,
|
||||
questionPrefix,
|
||||
questionSuffix,
|
||||
options,
|
||||
blanks,
|
||||
value,
|
||||
error,
|
||||
feedbackType = 'error',
|
||||
onChange,
|
||||
}: DragQuizPanelProps) => {
|
||||
const prevVisibleRef = useRef(visible)
|
||||
|
||||
useEffect(() => {
|
||||
if (prevVisibleRef.current && !visible && value.length > 0) {
|
||||
onChange([])
|
||||
}
|
||||
prevVisibleRef.current = visible
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
const filledValues = new Set(value.map((r) => r.optionValue))
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
|
||||
<div className="relative w-[1728px] h-[972px] flex flex-col items-center pt-12 pb-14">
|
||||
{/* 题干卡片 */}
|
||||
<div className="w-[1700px] rounded-[32px] bg-black/90 px-20 py-12 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
|
||||
<div className="text-[32px] leading-relaxed text-white">
|
||||
{title && <div className="mb-4">{title}</div>}
|
||||
<div>
|
||||
{questionPrefix && <span>{questionPrefix}</span>}
|
||||
{questionSuffix && <span>{questionSuffix}</span>}
|
||||
{error && (
|
||||
<div
|
||||
className={cn(
|
||||
'mt-5 text-center text-[26px]',
|
||||
feedbackType === 'success' ? 'text-[#b7f7d1]' : 'text-[#fcbe53]'
|
||||
)}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 左右布局:篮子 + 选项 */}
|
||||
<div className="w-[1700px] mt-10 flex gap-10 flex-1 min-h-0">
|
||||
{/* 左侧:篮子区域 - 每个 blank 都需要 data-blank-id 以支持多选项放入 */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-[800px] rounded-[32px]',
|
||||
'border-4 border-dashed border-[#d4c5a9]/60',
|
||||
'bg-white/5 backdrop-blur-sm',
|
||||
'flex flex-wrap content-center items-center justify-center gap-4 px-8 py-6',
|
||||
'transition-all duration-300'
|
||||
)}
|
||||
>
|
||||
{/* 隐藏的 blank 锚点,让每个 blank 都能被 DragFill 检测到 */}
|
||||
{blanks.map((blank) => {
|
||||
const isFilled = value.some((r) => r.blankId === blank.id)
|
||||
if (isFilled) return null
|
||||
return (
|
||||
<div
|
||||
key={blank.id}
|
||||
data-blank-id={blank.id}
|
||||
className="absolute inset-0 rounded-[32px]"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{value.length === 0 ? (
|
||||
<span className="text-white/30 text-[24px]">请将正确选项拖入此处</span>
|
||||
) : (
|
||||
value.map((result) => {
|
||||
const opt = options.find((o) => o.value === result.optionValue)
|
||||
return (
|
||||
<div
|
||||
key={result.blankId + '-' + result.optionValue}
|
||||
className={cn(
|
||||
'relative px-8 py-4 flex items-center justify-center',
|
||||
'rounded-[24px] border-3 border-[#f0be75]',
|
||||
'bg-linear-to-br from-[#4a3f35] to-[#3a2f25]',
|
||||
'text-[24px] font-bold text-[#f5e6d3]',
|
||||
'shadow-[0_6px_24px_rgba(240,190,117,0.3)]',
|
||||
'gap-4 animate-in fade-in zoom-in duration-300'
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 rounded-[24px] bg-linear-to-br from-[#d4c5a9]/15 to-transparent pointer-events-none" />
|
||||
<span className="relative z-10 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)]">
|
||||
{opt?.label || result.optionValue}
|
||||
</span>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="relative z-10 w-7 h-7 flex items-center justify-center rounded-full bg-[#f0be75]/20 hover:bg-[#f0be75]/40 text-[#f0be75] text-sm transition-all hover:scale-110"
|
||||
onClick={() => {
|
||||
onChange(value.filter((r) => r.blankId !== result.blankId))
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:选项列表,每个选项单独一行 */}
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<DragFill
|
||||
options={options}
|
||||
blanks={blanks}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hideLabels
|
||||
showSubmitButton={false}
|
||||
layout="vertical"
|
||||
className="w-full h-full"
|
||||
optionClassName="flex flex-col gap-6"
|
||||
renderOption={(option, isUsed) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-h-[90px] px-10 flex items-center justify-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)
|
||||
? '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]'
|
||||
)}
|
||||
>
|
||||
{!(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">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
renderDragging={(option) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-h-[60px] px-10 flex items-center justify-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]',
|
||||
'scale-105 opacity-90'
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
renderBlank={() => <div className="hidden" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BasketQuizPanel
|
||||
@@ -12,6 +12,7 @@ const DragQuizPanel = ({
|
||||
questionSuffix,
|
||||
options,
|
||||
blanks,
|
||||
blankPrefixes,
|
||||
blankSeparators,
|
||||
value,
|
||||
error,
|
||||
@@ -47,6 +48,7 @@ const DragQuizPanel = ({
|
||||
const filledValue = filledResult?.optionValue || null
|
||||
return (
|
||||
<span key={blank.id}>
|
||||
{blankPrefixes?.[index] && <span>{blankPrefixes[index]}</span>}
|
||||
<span
|
||||
data-blank-id={blank.id}
|
||||
className={cn(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { MatchQuizPanelProps } from './types'
|
||||
|
||||
@@ -45,8 +45,17 @@ const MatchQuizPanel = ({
|
||||
const [selectedLeftId, setSelectedLeftId] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string>('')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const middleRef = useRef<HTMLDivElement>(null)
|
||||
const leftColRef = useRef<HTMLDivElement>(null)
|
||||
const rightColRef = useRef<HTMLDivElement>(null)
|
||||
const leftItemRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const rightItemRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
// 当前错误的连线对,用于做红色高亮+抖动反馈
|
||||
const [errorPairs, setErrorPairs] = useState<Array<{ leftId: string; rightId: string }>>([])
|
||||
const [leftCenters, setLeftCenters] = useState<Record<string, number>>({})
|
||||
const [rightCenters, setRightCenters] = useState<Record<string, number>>({})
|
||||
const [middleHeight, setMiddleHeight] = useState<number>(300)
|
||||
const rafIdRef = useRef<number | null>(null)
|
||||
|
||||
// 当窗口关闭时,重置状态
|
||||
useEffect(() => {
|
||||
@@ -163,17 +172,79 @@ const MatchQuizPanel = ({
|
||||
return matches.some((m) => m.rightId === rightId)
|
||||
}
|
||||
|
||||
// 获取左项对应的右项ID
|
||||
const getMatchedRightId = (leftId: string) => {
|
||||
const match = matches.find((m) => m.leftId === leftId)
|
||||
return match?.rightId || null
|
||||
}
|
||||
const measureCenters = useCallback(() => {
|
||||
const middleEl = middleRef.current
|
||||
if (!middleEl) return
|
||||
|
||||
// 获取右项对应的左项ID
|
||||
const getMatchedLeftId = (rightId: string) => {
|
||||
const match = matches.find((m) => m.rightId === rightId)
|
||||
return match?.leftId || null
|
||||
}
|
||||
const middleRect = middleEl.getBoundingClientRect()
|
||||
const nextLeft: Record<string, number> = {}
|
||||
const nextRight: Record<string, number> = {}
|
||||
|
||||
// 显式同步中间连线层高度,避免因 flex/stretch/内容换行导致坐标系基准漂移
|
||||
const leftRect = leftColRef.current?.getBoundingClientRect()
|
||||
const rightRect = rightColRef.current?.getBoundingClientRect()
|
||||
const nextMiddleHeight = Math.max(leftRect?.height ?? 0, rightRect?.height ?? 0, 300)
|
||||
setMiddleHeight(nextMiddleHeight)
|
||||
|
||||
for (const item of leftItems) {
|
||||
const el = leftItemRefs.current[item.id]
|
||||
if (!el) continue
|
||||
const rect = el.getBoundingClientRect()
|
||||
nextLeft[item.id] = rect.top + rect.height / 2 - middleRect.top
|
||||
}
|
||||
|
||||
for (const item of rightItems) {
|
||||
const el = rightItemRefs.current[item.id]
|
||||
if (!el) continue
|
||||
const rect = el.getBoundingClientRect()
|
||||
nextRight[item.id] = rect.top + rect.height / 2 - middleRect.top
|
||||
}
|
||||
|
||||
setLeftCenters(nextLeft)
|
||||
setRightCenters(nextRight)
|
||||
}, [leftItems, rightItems])
|
||||
|
||||
const scheduleMeasure = useCallback(() => {
|
||||
if (rafIdRef.current !== null) {
|
||||
cancelAnimationFrame(rafIdRef.current)
|
||||
rafIdRef.current = null
|
||||
}
|
||||
// 双 RAF:等待浏览器完成一帧布局/绘制(含 transform/transition/pulse 的本帧生效)
|
||||
rafIdRef.current = requestAnimationFrame(() => {
|
||||
rafIdRef.current = requestAnimationFrame(() => {
|
||||
rafIdRef.current = null
|
||||
measureCenters()
|
||||
})
|
||||
})
|
||||
}, [measureCenters])
|
||||
|
||||
// 首次展示、匹配变化、选中/错误状态变化都会影响位置(含动画/缩放),延后一帧再测
|
||||
useEffect(() => {
|
||||
if (!visible) return
|
||||
scheduleMeasure()
|
||||
}, [visible, matches.length, errorPairs.length, selectedLeftId, scheduleMeasure])
|
||||
|
||||
// 监听尺寸变化(窗口 resize / 容器尺寸变化 / 文本换行)
|
||||
useEffect(() => {
|
||||
if (!visible) return
|
||||
const middleEl = middleRef.current
|
||||
if (!middleEl) return
|
||||
|
||||
const handleResize = () => scheduleMeasure()
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
const ro = new ResizeObserver(() => scheduleMeasure())
|
||||
ro.observe(middleEl)
|
||||
if (containerRef.current) ro.observe(containerRef.current)
|
||||
if (leftColRef.current) ro.observe(leftColRef.current)
|
||||
if (rightColRef.current) ro.observe(rightColRef.current)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current)
|
||||
ro.disconnect()
|
||||
}
|
||||
}, [visible, scheduleMeasure])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
@@ -199,9 +270,9 @@ const MatchQuizPanel = ({
|
||||
</div>
|
||||
|
||||
{/* 连线区域 */}
|
||||
<div className="w-full max-w-[1600px] flex flex-col md:flex-row gap-4 md:gap-8 items-start md:items-center justify-center">
|
||||
<div className="w-full max-w-[1600px] flex flex-col md:flex-row gap-4 md:gap-8 items-start md:items-stretch justify-center">
|
||||
{/* 左列:日期 */}
|
||||
<div className="w-full md:w-1/2 flex flex-col gap-3 md:gap-4">
|
||||
<div ref={leftColRef} className="w-full md:w-1/2 flex flex-col gap-3 md:gap-4">
|
||||
{leftItems.map((item) => {
|
||||
const isMatched = isLeftItemMatched(item.id)
|
||||
const isSelected = selectedLeftId === item.id
|
||||
@@ -210,6 +281,9 @@ const MatchQuizPanel = ({
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
ref={(el) => {
|
||||
leftItemRefs.current[item.id] = el
|
||||
}}
|
||||
className={cn(
|
||||
'relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200',
|
||||
'text-[20px] md:text-[28px] leading-relaxed cursor-pointer',
|
||||
@@ -221,7 +295,7 @@ const MatchQuizPanel = ({
|
||||
? 'bg-[#d4a574] text-[#5c3e2e] border-[#f0be75] shadow-lg scale-105'
|
||||
: isMatched
|
||||
? 'bg-[#3a3020] text-[#d4c5a9] border-[#8b7355]'
|
||||
: 'bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
|
||||
: 'bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-linear-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
|
||||
)}
|
||||
onClick={() => handleLeftItemClick(item.id)}
|
||||
onTouchStart={(e) => {
|
||||
@@ -249,7 +323,11 @@ const MatchQuizPanel = ({
|
||||
</div>
|
||||
|
||||
{/* 中间连线区域(移动端隐藏,桌面端显示) */}
|
||||
<div className="hidden md:block w-16 h-full min-h-[300px] relative flex-shrink-0">
|
||||
<div
|
||||
ref={middleRef}
|
||||
className="hidden md:block w-16 relative shrink-0"
|
||||
style={{ height: middleHeight }}
|
||||
>
|
||||
{/* 绘制连线 */}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none overflow-visible">
|
||||
{matches.map((match, index) => {
|
||||
@@ -257,14 +335,9 @@ const MatchQuizPanel = ({
|
||||
const rightItem = rightItems.find((item) => item.id === match.rightId)
|
||||
if (!leftItem || !rightItem) return null
|
||||
|
||||
const leftIndex = leftItems.findIndex((item) => item.id === match.leftId)
|
||||
const rightIndex = rightItems.findIndex((item) => item.id === match.rightId)
|
||||
|
||||
// 计算连线起点和终点(基于项目高度和间距)
|
||||
const itemHeight = 80 // 每个项目的大概高度
|
||||
const gap = 16 // 项目之间的间距
|
||||
const startY = leftIndex * (itemHeight + gap) + itemHeight / 2
|
||||
const endY = rightIndex * (itemHeight + gap) + itemHeight / 2
|
||||
const startY = leftCenters[match.leftId]
|
||||
const endY = rightCenters[match.rightId]
|
||||
if (startY === undefined || endY === undefined) return null
|
||||
|
||||
return (
|
||||
<line
|
||||
@@ -297,7 +370,7 @@ const MatchQuizPanel = ({
|
||||
</div>
|
||||
|
||||
{/* 右列:事件 */}
|
||||
<div className="w-full md:w-1/2 flex flex-col gap-3 md:gap-4">
|
||||
<div ref={rightColRef} className="w-full md:w-1/2 flex flex-col gap-3 md:gap-4">
|
||||
{rightItems.map((item) => {
|
||||
const isMatched = isRightItemMatched(item.id)
|
||||
const isError = errorPairs.some((p) => p.rightId === item.id)
|
||||
@@ -305,6 +378,9 @@ const MatchQuizPanel = ({
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
ref={(el) => {
|
||||
rightItemRefs.current[item.id] = el
|
||||
}}
|
||||
className={cn(
|
||||
'relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200',
|
||||
'text-[20px] md:text-[28px] leading-relaxed',
|
||||
@@ -319,7 +395,7 @@ const MatchQuizPanel = ({
|
||||
: isMatched
|
||||
? 'bg-[#3a3020] text-[#d4c5a9] border-[#8b7355]'
|
||||
: selectedLeftId
|
||||
? 'bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
|
||||
? 'bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-linear-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
|
||||
: 'bg-[#d7b888]/60 text-[#5c3e2e]/60 border-[#d4c5a9]/60'
|
||||
)}
|
||||
onClick={() => handleRightItemClick(item.id)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SingleChoice } from '@/components'
|
||||
import SuccessAlert from '@/components/layout/alert/success-alert'
|
||||
import type { SingleChoiceQuizPanelProps } from './types'
|
||||
|
||||
/**
|
||||
@@ -38,20 +39,38 @@ const SingleChoiceQuizPanel = ({
|
||||
onWrongSelect,
|
||||
onError,
|
||||
disabled = false,
|
||||
showSuccessAlert = false,
|
||||
successAlertProps,
|
||||
onSuccessContinue,
|
||||
}: SingleChoiceQuizPanelProps) => {
|
||||
const [selectedValue, setSelectedValue] = useState<string>('')
|
||||
const [error, setError] = useState<string>('')
|
||||
const [successAlertVisible, setSuccessAlertVisible] = useState<boolean>(false)
|
||||
|
||||
// 当窗口关闭时,重置内部状态
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedValue('')
|
||||
setError('')
|
||||
setSuccessAlertVisible(false)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
// 当成功弹窗显示时,隐藏题目面板,避免双层遮罩叠加
|
||||
const shouldRenderQuizPanel = visible && !successAlertVisible
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
// 约定:显式开启,或传入 title/content 任一文案,即启用答对成功弹窗
|
||||
const hasSuccessAlertCopy =
|
||||
Boolean(successAlertProps?.title) || Boolean(successAlertProps?.content)
|
||||
const shouldShowSuccessAlert = showSuccessAlert || hasSuccessAlertCopy
|
||||
|
||||
const handleSuccessContinue = () => {
|
||||
setSuccessAlertVisible(false)
|
||||
onSuccessContinue?.()
|
||||
}
|
||||
|
||||
// 处理选项点击
|
||||
const handleOptionClick = (value: string) => {
|
||||
if (disabled) return
|
||||
@@ -69,6 +88,10 @@ const SingleChoiceQuizPanel = ({
|
||||
setSelectedValue('')
|
||||
setError('')
|
||||
onSubmit?.(value, true)
|
||||
|
||||
if (shouldShowSuccessAlert) {
|
||||
setSuccessAlertVisible(true)
|
||||
}
|
||||
} else {
|
||||
// 答题失败:清空状态,执行 onError,允许重新选择
|
||||
setSelectedValue('')
|
||||
@@ -86,45 +109,61 @@ const SingleChoiceQuizPanel = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
|
||||
<div className="relative w-[1728px] h-[972px] flex flex-col items-center justify-center">
|
||||
{/* 题干卡片 */}
|
||||
<div className="w-[1700px] rounded-[32px] bg-black px-20 py-8 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
|
||||
<div className="text-[34px] leading-relaxed text-white">{title}</div>
|
||||
<>
|
||||
{shouldRenderQuizPanel && (
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center bg-black/40"
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<div className="relative w-[1728px] h-[972px] flex flex-col items-center justify-center">
|
||||
{/* 题干卡片 */}
|
||||
<div className="w-[1700px] rounded-[32px] bg-black px-20 py-8 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
|
||||
<div className="text-[34px] leading-relaxed text-white">{title}</div>
|
||||
|
||||
{/* 答案反馈 */}
|
||||
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
|
||||
</div>
|
||||
{/* 答案反馈 */}
|
||||
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
|
||||
</div>
|
||||
|
||||
{/* 选项区域 */}
|
||||
<div className="mt-16">
|
||||
<SingleChoice
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
disabled={disabled}
|
||||
renderOption={(option, isSelected) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="mb-6 cursor-pointer"
|
||||
onClick={() => handleOptionClick(option.value)}
|
||||
>
|
||||
<div
|
||||
className={`w-[1500px] flex items-center p-4 text-[28px] leading-relaxed rounded-tr-[50px] rounded-bl-4xl transition-all duration-150 ${
|
||||
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'
|
||||
} ${
|
||||
isSelected
|
||||
? 'bg-[#d4a574] text-[#5c3e2e]'
|
||||
: 'bg-[#d7b888] text-[#5c3e2e] hover:bg-gradient-to-r from-[#ddf3e1] to-[#fbdd97]'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{/* 选项区域 */}
|
||||
<div className="mt-16">
|
||||
<SingleChoice
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
disabled={disabled}
|
||||
renderOption={(option, isSelected) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="mb-6 cursor-pointer"
|
||||
onClick={() => handleOptionClick(option.value)}
|
||||
>
|
||||
<div
|
||||
className={`w-[1500px] flex items-center p-4 text-[28px] leading-relaxed rounded-tr-[50px] rounded-bl-4xl transition-all duration-150 ${
|
||||
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'
|
||||
} ${
|
||||
isSelected
|
||||
? 'bg-[#d4a574] text-[#5c3e2e]'
|
||||
: 'bg-[#d7b888] text-[#5c3e2e] hover:bg-linear-to-r from-[#ddf3e1] to-[#fbdd97]'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowSuccessAlert && (
|
||||
<SuccessAlert
|
||||
{...successAlertProps}
|
||||
visible={successAlertVisible}
|
||||
onContinue={handleSuccessContinue}
|
||||
onClose={handleSuccessContinue}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export { default as SingleChoiceQuizPanel } from './SingleChoiceQuizPanel'
|
||||
export { default as MultipleChoiceQuizPanel } from './MultipleChoiceQuizPanel'
|
||||
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 type {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DragFillResult,
|
||||
DragFillBlank,
|
||||
} from '@/components'
|
||||
import type { SuccessAlertProps } from '@/components/layout/alert/success-alert'
|
||||
|
||||
/** 拖拽题面板配置 */
|
||||
export interface DragQuizPanelProps {
|
||||
@@ -21,12 +22,19 @@ export interface DragQuizPanelProps {
|
||||
options: { value: string; label: string }[]
|
||||
/** 空白区域列表 */
|
||||
blanks: DragFillBlank[]
|
||||
/** 每个空之前的前缀文本(按 blanks 顺序一一对应) */
|
||||
blankPrefixes?: string[]
|
||||
/** 空白之间的分隔符数组(可选,如果不提供则使用默认的"、") */
|
||||
blankSeparators?: string[]
|
||||
/** 当前值 */
|
||||
value: DragFillResult[]
|
||||
/** 错误提示信息 */
|
||||
/**
|
||||
* 提示信息(历史上叫 error,但实际也可能用来展示正确提示)
|
||||
* 由 feedbackType 控制样式
|
||||
*/
|
||||
error?: string
|
||||
/** 提示类型:success 为正确提示,error 为错误提示 */
|
||||
feedbackType?: 'success' | 'error'
|
||||
/** 值变化回调 */
|
||||
onChange: (value: DragFillResult[]) => void
|
||||
}
|
||||
@@ -53,6 +61,12 @@ export interface SingleChoiceQuizPanelProps {
|
||||
onError?: (error: string) => void
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
/** 答对后是否自动显示 SuccessAlert(默认 false;当 successAlertProps.title/content 有值时也会自动显示) */
|
||||
showSuccessAlert?: boolean
|
||||
/** SuccessAlert 的可配置项(除 visible / onContinue / onClose 外) */
|
||||
successAlertProps?: Omit<SuccessAlertProps, 'visible' | 'onContinue' | 'onClose'>
|
||||
/** 答对弹窗点击继续回调 */
|
||||
onSuccessContinue?: () => void
|
||||
}
|
||||
|
||||
/** 多选题面板配置 */
|
||||
|
||||
Reference in new issue
Block a user