feat: update residence learning flows
Commit non-media application and asset changes while leaving audio, video, and environment files out of version control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
68ba22a33f
commit
6d40e19346
184 files changed
+7663
-2203
No files matched your search
@@ -47,30 +47,25 @@ src/components/
|
||||
## 组件模板
|
||||
|
||||
````tsx
|
||||
import React, {
|
||||
forwardRef,
|
||||
useRef,
|
||||
useState,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { forwardRef, useRef, useState, useImperativeHandle } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** 组件配置项 */
|
||||
export interface ComponentNameProps {
|
||||
/** 必填属性说明 */
|
||||
requiredProp: string;
|
||||
requiredProp: string
|
||||
/** 可选属性说明 */
|
||||
optionalProp?: boolean;
|
||||
optionalProp?: boolean
|
||||
/** 子元素 */
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode
|
||||
/** 自定义类名 */
|
||||
className?: string;
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** 组件 Ref 暴露的方法 */
|
||||
export interface ComponentNameRef {
|
||||
/** 方法说明 */
|
||||
methodName: () => void;
|
||||
methodName: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,33 +79,33 @@ export interface ComponentNameRef {
|
||||
const ComponentName = forwardRef<ComponentNameRef, ComponentNameProps>(
|
||||
({ requiredProp, optionalProp = false, className, children }, ref) => {
|
||||
// 状态定义
|
||||
const [state, setState] = useState<string>("");
|
||||
const [state, setState] = useState<string>('')
|
||||
|
||||
// Ref 定义
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
methodName: () => {
|
||||
// 实现
|
||||
},
|
||||
}));
|
||||
}))
|
||||
|
||||
return <div className={cn("base-class", className)}>{children}</div>;
|
||||
return <div className={cn('base-class', className)}>{children}</div>
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
ComponentName.displayName = "ComponentName";
|
||||
ComponentName.displayName = 'ComponentName'
|
||||
|
||||
export default ComponentName;
|
||||
export default ComponentName
|
||||
````
|
||||
|
||||
## index.ts 导出模板
|
||||
|
||||
```ts
|
||||
export { default } from "./component-name";
|
||||
export { default as ComponentName } from "./component-name";
|
||||
export type { ComponentNameProps, ComponentNameRef } from "./component-name";
|
||||
export { default } from './component-name'
|
||||
export { default as ComponentName } from './component-name'
|
||||
export type { ComponentNameProps, ComponentNameRef } from './component-name'
|
||||
```
|
||||
|
||||
## 类型规范
|
||||
|
||||
@@ -79,6 +79,9 @@ const ExhibitOverlay = ({
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const viewElRef = useRef<HTMLDivElement | null>(null)
|
||||
const frameRef = useRef<number | null>(null)
|
||||
const openTimerRef = useRef<number | null>(null)
|
||||
const wasVisibleRef = useRef(false)
|
||||
const onOpenRef = useRef(onOpen)
|
||||
const pointers = useRef<Map<number, Point>>(new Map())
|
||||
const lastDistance = useRef<number | null>(null)
|
||||
// 拖拽初始状态:记录 pointerDown 时的状态
|
||||
@@ -137,10 +140,25 @@ const ExhibitOverlay = ({
|
||||
}, [applyTransformStyle])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
onOpen?.()
|
||||
onOpenRef.current = onOpen
|
||||
}, [onOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && !wasVisibleRef.current) {
|
||||
openTimerRef.current = window.setTimeout(() => {
|
||||
onOpenRef.current?.()
|
||||
openTimerRef.current = null
|
||||
}, 0)
|
||||
}
|
||||
}, [visible, onOpen])
|
||||
wasVisibleRef.current = visible
|
||||
|
||||
return () => {
|
||||
if (openTimerRef.current) {
|
||||
window.clearTimeout(openTimerRef.current)
|
||||
openTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
// 关闭或切换时重置
|
||||
useEffect(() => {
|
||||
|
||||
@@ -149,6 +149,13 @@ export const StudyGuideModal = ({
|
||||
// }
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (audioSrc && currentSrc === audioSrc) {
|
||||
stop()
|
||||
}
|
||||
onContinue?.()
|
||||
}
|
||||
|
||||
const contentChildren = renderIndentedContent(children, firstLineIndent)
|
||||
|
||||
const content = (
|
||||
@@ -204,7 +211,7 @@ export const StudyGuideModal = ({
|
||||
src={getStaticImage('components/guide/继续')}
|
||||
alt="继续"
|
||||
button
|
||||
onClick={onContinue}
|
||||
onClick={handleContinue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const RESIDENCES_WITH_PUZZLE = [
|
||||
'maozedong',
|
||||
'nieer',
|
||||
'zhangleping',
|
||||
'rongdesheng',
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -186,48 +187,52 @@ const ResidenceLayoutContent = () => {
|
||||
// 处理碎片动画触发(仅支持拼图功能的故居)
|
||||
const handleTriggerPuzzleAnimation = useCallback(
|
||||
(pieceId: number, options?: { onComplete?: () => void }) => {
|
||||
if (!residence || !hasPuzzleFeature) {
|
||||
options?.onComplete?.()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// 防止重复执行动画
|
||||
if (animationInProgressRef.current) {
|
||||
options?.onComplete?.()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// 检查碎片是否已解锁
|
||||
if (isPuzzleUnlocked(residence, pieceId)) {
|
||||
options?.onComplete?.()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// 标记动画正在进行
|
||||
animationInProgressRef.current = true
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
animationCompletionRef.current = resolve
|
||||
animationCompletionCallbackRef.current = options?.onComplete ?? null
|
||||
window.setTimeout(() => {
|
||||
if (!residence || !hasPuzzleFeature) {
|
||||
options?.onComplete?.()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 开始动画流程:禁用所有操作
|
||||
setIsAnimationPlaying(true)
|
||||
setPuzzlePieceId(pieceId)
|
||||
setShowPuzzlePiece(true)
|
||||
// 防止重复执行动画
|
||||
if (animationInProgressRef.current) {
|
||||
options?.onComplete?.()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 使用双重 requestAnimationFrame 确保 Portal 渲染完成
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
// 等待初始出现动画(旋转+放大)完成
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.APPEAR_DURATION))
|
||||
// 检查碎片是否已解锁
|
||||
if (isPuzzleUnlocked(residence, pieceId)) {
|
||||
options?.onComplete?.()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 等待发光动画显示一段时间,让用户看到碎片
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.GLOW_DURATION))
|
||||
// 标记动画正在进行
|
||||
animationInProgressRef.current = true
|
||||
animationCompletionRef.current = resolve
|
||||
animationCompletionCallbackRef.current = options?.onComplete ?? null
|
||||
|
||||
// 等待元素准备就绪并执行飞行动画
|
||||
await waitForElementsAndAnimate(pieceId)
|
||||
// 开始动画流程:禁用所有操作
|
||||
setIsAnimationPlaying(true)
|
||||
setPuzzlePieceId(pieceId)
|
||||
setShowPuzzlePiece(true)
|
||||
|
||||
// 使用双重 requestAnimationFrame 确保 Portal 渲染完成
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
// 等待初始出现动画(旋转+放大)完成
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.APPEAR_DURATION))
|
||||
|
||||
// 等待发光动画显示一段时间,让用户看到碎片
|
||||
await new Promise((resolve) => setTimeout(resolve, ANIMATION_CONFIG.GLOW_DURATION))
|
||||
|
||||
// 等待元素准备就绪并执行飞行动画
|
||||
await waitForElementsAndAnimate(pieceId)
|
||||
})
|
||||
})
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
},
|
||||
[
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createPortal } from 'react-dom'
|
||||
import { useScale } from '@/contexts/ScaleContext'
|
||||
|
||||
export interface ScaledPortalProps {
|
||||
/** 是否显示 */
|
||||
show?: boolean
|
||||
/** 是否启用(不启用则直接渲染 children) */
|
||||
enabled?: boolean
|
||||
/** z-index 层级(建议 > ExhibitOverlay 的 9999) */
|
||||
@@ -17,7 +19,12 @@ export interface ScaledPortalProps {
|
||||
* - 使用 Portal 渲染到 document.body,避免被其他 Portal/遮罩层挡住
|
||||
* - 使用 useScale 的 offset/scale 做视觉对齐(与 ExhibitOverlay 一致)
|
||||
*/
|
||||
const ScaledPortal = ({ enabled = true, zIndex = 10050, children }: ScaledPortalProps) => {
|
||||
const ScaledPortal = ({
|
||||
enabled = true,
|
||||
zIndex = 10050,
|
||||
show = true,
|
||||
children,
|
||||
}: ScaledPortalProps) => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale()
|
||||
|
||||
@@ -25,6 +32,7 @@ const ScaledPortal = ({ enabled = true, zIndex = 10050, children }: ScaledPortal
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!show) return null
|
||||
if (!enabled) return <>{children}</>
|
||||
if (!mounted) return null
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ 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">{text}</div>
|
||||
<div className="text-white text-5xl font-medium tracking-widest leading-18">{text}</div>
|
||||
</div>
|
||||
|
||||
{/* 继续按钮 */}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface SuccessAlertProps {
|
||||
* 使用 components/正确版 作为背景
|
||||
*/
|
||||
const SuccessAlert = ({
|
||||
title = '恭喜你, 选择正确!',
|
||||
title = '恭喜你,选择正确!',
|
||||
content,
|
||||
visible = true,
|
||||
audioSrc,
|
||||
@@ -108,16 +108,12 @@ const SuccessAlert = ({
|
||||
if (audioSrc && currentSrc === audioSrc) {
|
||||
stop()
|
||||
}
|
||||
// 只调用 onContinue,它会处理关闭逻辑
|
||||
if (onContinue) {
|
||||
onContinue()
|
||||
} else if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
onContinue?.()
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center">
|
||||
<div className="fixed inset-0 z-10000 flex items-center justify-center">
|
||||
{/* 蒙版背景 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
|
||||
@@ -55,7 +55,7 @@ const DragQuizPanel = ({
|
||||
'inline-flex items-center justify-center min-w-[140px] h-[56px] px-6 mx-3',
|
||||
'rounded-xl border-2 transition-all duration-300 shadow-[0_4px_16px_rgba(0,0,0,0.2)]',
|
||||
filledValue
|
||||
? 'border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] backdrop-blur-sm'
|
||||
? 'border-[#f0be75] bg-linear-to-br from-[#3a3020] to-[#2a2520] backdrop-blur-sm'
|
||||
: 'border-[#d4c5a9]/60 bg-white/10 backdrop-blur-sm hover:border-[#d4c5a9]'
|
||||
)}
|
||||
>
|
||||
@@ -111,11 +111,11 @@ const DragQuizPanel = ({
|
||||
'text-[32px] font-bold border-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)]',
|
||||
isUsed
|
||||
? 'border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50'
|
||||
: 'border-[#d4c5a9] bg-gradient-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_12px_40px_rgba(240,190,117,0.7)] hover:scale-[1.02]'
|
||||
)}
|
||||
>
|
||||
{!isUsed && (
|
||||
<div className="absolute inset-0 rounded-[32px] bg-gradient-to-br from-[#d4c5a9]/20 to-transparent pointer-events-none" />
|
||||
<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}
|
||||
|
||||
@@ -22,57 +22,55 @@
|
||||
### 使用示例
|
||||
|
||||
```tsx
|
||||
import { SelectFillQuizPanel } from "@/components";
|
||||
import { SelectFillQuizPanel } from '@/components'
|
||||
|
||||
function MyComponent() {
|
||||
const [showQuiz, setShowQuiz] = useState(false);
|
||||
const [showQuiz, setShowQuiz] = useState(false)
|
||||
|
||||
return (
|
||||
<SelectFillQuizPanel
|
||||
visible={showQuiz}
|
||||
titlePrefix="【填空】毛泽东主席认为,到了"
|
||||
titleSuffix="年夏天,在理论上,而且在某种程度的行动上,他已成为一个马克思主义者。"
|
||||
blanks={[
|
||||
{ id: "year", prefix: "____" },
|
||||
]}
|
||||
blanks={[{ id: 'year', prefix: '____' }]}
|
||||
options={[
|
||||
{ value: "1919", label: "1919" },
|
||||
{ value: "1920", label: "1920" },
|
||||
{ value: "1921", label: "1921" },
|
||||
{ value: '1919', label: '1919' },
|
||||
{ value: '1920', label: '1920' },
|
||||
{ value: '1921', label: '1921' },
|
||||
]}
|
||||
correctAnswers={{ year: "1920" }}
|
||||
correctAnswers={{ year: '1920' }}
|
||||
errorMessage="回想一下 1920 年 5 月到 7 月在上海居住的这段经历~"
|
||||
onSubmit={(results, isCorrect) => {
|
||||
console.log("选择结果:", results, "是否正确:", isCorrect);
|
||||
console.log('选择结果:', results, '是否正确:', isCorrect)
|
||||
if (isCorrect) {
|
||||
setShowQuiz(false);
|
||||
setShowQuiz(false)
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.log("错误信息:", error);
|
||||
console.log('错误信息:', error)
|
||||
}}
|
||||
submitText="确定"
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| titlePrefix | string | - | 整体题干前缀文本(可选) |
|
||||
| titleSuffix | string | - | 整体题干后缀文本(可选) |
|
||||
| blanks | SelectFillBlank[] | - | 每个填空位的配置(必填,包含 id / prefix / suffix) |
|
||||
| options | { value: string; label: string }[] | - | 候选项列表(必填) |
|
||||
| correctAnswers | Record<string, string> | - | 正确答案映射(可选,不传则不做校验,直接判定为正确) |
|
||||
| errorMessage | string | `"还不太对,再仔细对照题干和提示想一想吧~"` | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (results: SelectFillResult[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| -------------- | --------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------- |
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| titlePrefix | string | - | 整体题干前缀文本(可选) |
|
||||
| titleSuffix | string | - | 整体题干后缀文本(可选) |
|
||||
| blanks | SelectFillBlank[] | - | 每个填空位的配置(必填,包含 id / prefix / suffix) |
|
||||
| options | { value: string; label: string }[] | - | 候选项列表(必填) |
|
||||
| correctAnswers | Record<string, string> | - | 正确答案映射(可选,不传则不做校验,直接判定为正确) |
|
||||
| errorMessage | string | `"还不太对,再仔细对照题干和提示想一想吧~"` | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (results: SelectFillResult[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
|
||||
### 行为说明
|
||||
|
||||
@@ -98,53 +96,53 @@ function MyComponent() {
|
||||
### 使用方法
|
||||
|
||||
```tsx
|
||||
import { SingleChoiceQuizPanel } from "@/components";
|
||||
import { SingleChoiceQuizPanel } from '@/components'
|
||||
|
||||
function MyComponent() {
|
||||
const [showQuiz, setShowQuiz] = useState(false);
|
||||
const [showQuiz, setShowQuiz] = useState(false)
|
||||
|
||||
return (
|
||||
<SingleChoiceQuizPanel
|
||||
visible={showQuiz}
|
||||
title="这是一道单选题:以下哪个选项是正确的?"
|
||||
options={[
|
||||
{ value: "A", label: "选项A" },
|
||||
{ value: "B", label: "选项B" },
|
||||
{ value: "C", label: "选项C" },
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
]}
|
||||
correctAnswer="B"
|
||||
errorMessage="再想一想,注意题干中的时间和提示信息哦~"
|
||||
onSubmit={(value, isCorrect) => {
|
||||
console.log("用户选择:", value);
|
||||
console.log("是否正确:", isCorrect);
|
||||
console.log('用户选择:', value)
|
||||
console.log('是否正确:', isCorrect)
|
||||
if (isCorrect) {
|
||||
// 选择正确,可以关闭面板或进入下一步
|
||||
setShowQuiz(false);
|
||||
setShowQuiz(false)
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.log("错误信息:", error);
|
||||
console.log('错误信息:', error)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | SingleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string | - | 正确答案(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (value: string, isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ------------------------------------------- | -------------------------------------- | ------------------------------------ |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------ | ------ | -------- | ------ |
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | SingleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string | - | 正确答案(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| onSubmit | (value: string, isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
|
||||
### 行为说明
|
||||
|
||||
@@ -168,61 +166,61 @@ function MyComponent() {
|
||||
### 使用方法
|
||||
|
||||
```tsx
|
||||
import { MultipleChoiceQuizPanel } from "@/components";
|
||||
import { MultipleChoiceQuizPanel } from '@/components'
|
||||
|
||||
function MyComponent() {
|
||||
const [showQuiz, setShowQuiz] = useState(false);
|
||||
const [showQuiz, setShowQuiz] = useState(false)
|
||||
|
||||
return (
|
||||
<MultipleChoiceQuizPanel
|
||||
visible={showQuiz}
|
||||
title="这是一道多选题:以下哪些选项是正确的?(可多选)"
|
||||
options={[
|
||||
{ value: "A", label: "选项A" },
|
||||
{ value: "B", label: "选项B" },
|
||||
{ value: "C", label: "选项C" },
|
||||
{ value: "D", label: "选项D" },
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
{ value: 'D', label: '选项D' },
|
||||
]}
|
||||
correctAnswer={["A", "B"]}
|
||||
correctAnswer={['A', 'B']}
|
||||
errorMessage="很抱歉,你选错了!请你再思考一下吧!"
|
||||
minSelections={1}
|
||||
maxSelections={3}
|
||||
minSelectionsErrorMessage="请至少选择 1 项才能提交"
|
||||
submitText="确认提交"
|
||||
onSubmit={(values, isCorrect) => {
|
||||
console.log("用户选择:", values);
|
||||
console.log("是否正确:", isCorrect);
|
||||
console.log('用户选择:', values)
|
||||
console.log('是否正确:', isCorrect)
|
||||
if (isCorrect) {
|
||||
setShowQuiz(false);
|
||||
setShowQuiz(false)
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.log("错误信息:", error);
|
||||
console.log('错误信息:', error)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | MultipleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string[] | - | 正确答案数组(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| minSelectionsErrorMessage | string | `请至少选择 ${minSelections} 项` | 自定义最小选择数量不足时的提示信息 |
|
||||
| onSubmit | (values: string[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| minSelections | number | - | 最小选择数量 |
|
||||
| maxSelections | number | - | 最大选择数量 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------- | ---------------------------------------------- | -------------------------------------- | ------------------------------------ |
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ------ | ------ | -------- | ------ |
|
||||
| visible | boolean | - | 是否可见(必填) |
|
||||
| zIndex | number | 10000 | z-index 层级 |
|
||||
| title | string | - | 题目(必填) |
|
||||
| options | MultipleChoiceOption[] | - | 选项列表(必填) |
|
||||
| correctAnswer | string[] | - | 正确答案数组(可选,用于验证) |
|
||||
| errorMessage | string | "很抱歉,你选错了!请你再思考一下吧!" | 自定义错误提示信息(答案错误时显示) |
|
||||
| minSelectionsErrorMessage | string | `请至少选择 ${minSelections} 项` | 自定义最小选择数量不足时的提示信息 |
|
||||
| onSubmit | (values: string[], isCorrect: boolean) => void | - | 提交回调 |
|
||||
| onError | (error: string) => void | - | 错误回调 |
|
||||
| disabled | boolean | false | 是否禁用 |
|
||||
| minSelections | number | - | 最小选择数量 |
|
||||
| maxSelections | number | - | 最大选择数量 |
|
||||
| submitText | string | "提交" | 提交按钮文本 |
|
||||
|
||||
### 行为说明
|
||||
|
||||
@@ -237,6 +235,7 @@ function MyComponent() {
|
||||
### 从旧版 QuizPanel 迁移
|
||||
|
||||
**旧版用法:**
|
||||
|
||||
```tsx
|
||||
<QuizPanel
|
||||
visible={showQuiz}
|
||||
@@ -245,17 +244,18 @@ function MyComponent() {
|
||||
value={quizAnswer}
|
||||
error={quizError}
|
||||
onChange={(val) => {
|
||||
setQuizAnswer(val);
|
||||
if (val === "B") {
|
||||
setQuizError("");
|
||||
setQuizAnswer(val)
|
||||
if (val === 'B') {
|
||||
setQuizError('')
|
||||
} else {
|
||||
setQuizError("很抱歉,你选错了!");
|
||||
setQuizError('很抱歉,你选错了!')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**新版用法:**
|
||||
|
||||
```tsx
|
||||
<SingleChoiceQuizPanel
|
||||
visible={showQuiz}
|
||||
|
||||
@@ -159,7 +159,6 @@ const SingleChoiceQuizPanel = ({
|
||||
<SuccessAlert
|
||||
{...successAlertProps}
|
||||
visible={successAlertVisible}
|
||||
onContinue={handleSuccessContinue}
|
||||
onClose={handleSuccessContinue}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,9 +8,9 @@ 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}`
|
||||
const month = now.getMonth() + 1
|
||||
const day = now.getDate()
|
||||
return `${year}年${month}月${day}日`
|
||||
}
|
||||
|
||||
export type LetterFieldType = 'text' | 'input' | 'select'
|
||||
@@ -305,12 +305,14 @@ const LetterTemplate = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute right-[190px] bottom-[120px] z-99 flex flex-col items-end gap-2 text-[#4e402e]">
|
||||
<div className="absolute right-[190px] bottom-[120px] z-100 flex flex-col items-end gap-2 text-[#4e402e]">
|
||||
<style>{`.signature-name-input::placeholder { color: #999999; font-size: 26px; }`}</style>
|
||||
<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]'}`}
|
||||
className={`signature-name-input h-[36px] bg-transparent border-b-2 border-dashed text-center outline-none w-[240px] ${signatureError ? 'border-red-500' : 'border-[#4e402e]'}`}
|
||||
style={{ fontSize: '26px' }}
|
||||
autoComplete="off"
|
||||
placeholder="请郑重签下你的姓名"
|
||||
value={signatureName}
|
||||
maxLength={12}
|
||||
onChange={(e) => handleSignatureNameChange(e.target.value)}
|
||||
|
||||
@@ -82,7 +82,7 @@ const LetterToHuangyanpei = ({
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
onAnimationEnd={onAnimationEnd}
|
||||
title="寄给黄炎培的信"
|
||||
title="写给黄炎培先生"
|
||||
letterContent={letterContent}
|
||||
fields={fields}
|
||||
contentClassName="!text-[25px]"
|
||||
|
||||
@@ -63,7 +63,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
||||
correctValue: '横眉冷对',
|
||||
readOnlyFallback: '横眉冷对',
|
||||
hints: {
|
||||
error: '“横眉冷对于夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
error: '“横眉冷对千夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
},
|
||||
},
|
||||
4: {
|
||||
@@ -73,7 +73,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
||||
correctValue: '孺子牛',
|
||||
readOnlyFallback: '孺子牛',
|
||||
hints: {
|
||||
error: '“横眉冷对于夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
error: '“横眉冷对千夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
},
|
||||
},
|
||||
5: {
|
||||
|
||||
@@ -19,7 +19,7 @@ const commonSelectField: LetterFieldConfig = {
|
||||
className: '!w-[120px] !min-w-[120px] shrink-0 px-0',
|
||||
options: [
|
||||
{ value: '天下为公', label: '天下为公' },
|
||||
{ value: '敢为天下先', label: '敢为天下先' },
|
||||
{ value: '与时俱进', label: '与时俱进' },
|
||||
{ value: '振兴中华', label: '振兴中华' },
|
||||
],
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const commonSelectField: LetterFieldConfig = {
|
||||
const letterContent = `敬爱的孙中山先生:
|
||||
您好!
|
||||
|
||||
今天,我怀着崇敬的心情走进香山路 7 号,看着您亲手设计的房屋、勾画的《中国铁路全图》、撰写的《实业计划》手稿,仿佛能看到您为“振兴中华”奔走的身影。在李教授的解读中,我终于明白:您喊出的“____”,不是遥远的历史,而是我们这代人正在接续的接力;您追求的“____”,不是抽象的理念,而是从服务身边人、做好专业事开始的担当;您秉持的“____”,不是过去的选择,而是面对未来时必须有的勇气。
|
||||
今天,我怀着崇敬的心情走进香山路 7 号,看着您亲手设计的房屋、勾画的《中国铁路全图》、撰写的《实业计划》手稿,仿佛能看到您为“振兴中华”奔走的身影。在这场跨越时空的对话中,我终于明白,您喊出的“____”,不是遥远的历史,而是我们这代人正在接续的接力;您追求的“____”,不是抽象的理念,而是从服务身边人、做好专业事开始的担当;您秉持的“____”,不是过去的选择,而是面对未来时必须有的勇气。
|
||||
|
||||
我是____专业的学生,今后,我会以您“知难行易”的信念,在专业学习中攻克难题,就像您当年为了革命理想屡败屡战;我会以您“心系民众”的情怀,参与____,帮身边人解决实际困难;我更会记住您“适乎世界之潮流”的智慧,紧跟时代步伐,努力用____为民族复兴添砖加瓦。
|
||||
|
||||
@@ -52,11 +52,11 @@ const fields: Record<number, LetterFieldConfig> = {
|
||||
},
|
||||
2: {
|
||||
...commonSelectField,
|
||||
correctValue: '敢为天下先',
|
||||
correctValue: '与时俱进',
|
||||
hints: {
|
||||
error: '再想想哦,这里对应的是孙中山先生敢于探索新道路的胆识。',
|
||||
},
|
||||
readOnlyFallback: '敢为天下先',
|
||||
readOnlyFallback: '与时俱进',
|
||||
},
|
||||
3: {
|
||||
type: 'input',
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface NarrationProps {
|
||||
autoPlayAudio?: boolean
|
||||
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
|
||||
audioChannelId?: string
|
||||
/** 署名/出处,主文本动画结束后显示在右下方 */
|
||||
attribution?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,6 +43,7 @@ const Narration = ({
|
||||
animationSpeed = 100,
|
||||
autoPlayAudio = true,
|
||||
audioChannelId,
|
||||
attribution,
|
||||
}: NarrationProps) => {
|
||||
const [displayedText, setDisplayedText] = useState('')
|
||||
const [isAnimating, setIsAnimating] = useState(true)
|
||||
@@ -146,6 +149,7 @@ const Narration = ({
|
||||
|
||||
<div className="text-white text-3xl leading-relaxed space-y-8">
|
||||
<p className="whitespace-pre-wrap">{displayedText}</p>
|
||||
{attribution && !isAnimating && <p className="text-right">{attribution}</p>}
|
||||
</div>
|
||||
|
||||
{/* 关闭按钮 - 只在动画完成后显示 */}
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
import { Image } from '@/components'
|
||||
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||||
|
||||
/**
|
||||
* 六边形进度条组件
|
||||
* 根据进度值显示不同颜色:100%绿色,50%-99%橙色,0%-49%灰色
|
||||
*/
|
||||
interface HexagonProgressBarProps {
|
||||
progress: number // 0-100
|
||||
}
|
||||
|
||||
const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||||
const progressPercent = Math.min(100, Math.max(0, progress))
|
||||
const progressText =
|
||||
progressPercent.toFixed(2) === '100.00' ? '进度100%' : `进度${Math.round(progressPercent)}%`
|
||||
|
||||
// 根据进度值确定填充颜色
|
||||
const getProgressColor = () => {
|
||||
if (progressPercent >= 100) {
|
||||
return '#90B86B' // 浅橄榄绿色(100%完成)
|
||||
} else if (progressPercent >= 50) {
|
||||
return '#EEB362' // 橙色(50%-99%)
|
||||
} else {
|
||||
return '#E0E0E0' // 浅灰色(0%-49%)
|
||||
}
|
||||
}
|
||||
|
||||
const fillColor = getProgressColor()
|
||||
// 六边形路径,细长的水平六边形,左右两端是尖角
|
||||
// 正确的六边形路径:从左顶点开始,顺时针绘制
|
||||
// 左顶点(0%, 50%) -> 左上角(3%, 0%) -> 右上角(97%, 0%) -> 右顶点(100%, 50%) -> 右下角(97%, 100%) -> 左下角(3%, 100%)
|
||||
// 外层路径(包含边框)
|
||||
const outerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||||
// 内层路径(边框内部)
|
||||
const innerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||||
const borderWidth = 4 // 增加边框宽度从2px到3px
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '260px', height: '50px' }}>
|
||||
{/* 外层:白色边框(使用 clip-path 裁剪形状) */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
clipPath: outerHexagonPath,
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
/>
|
||||
{/* 内容容器(边框内部区域) */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${borderWidth}px`,
|
||||
top: `${borderWidth}px`,
|
||||
right: `${borderWidth}px`,
|
||||
bottom: `${borderWidth}px`,
|
||||
clipPath: innerHexagonPath,
|
||||
}}
|
||||
>
|
||||
{/* 背景层:深灰色背景(未填充部分) */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: '#757575',
|
||||
}}
|
||||
/>
|
||||
{/* 填充层(根据进度百分比从左侧填充) */}
|
||||
{progressPercent > 0 && (
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 overflow-hidden"
|
||||
style={{
|
||||
width: `${progressPercent}%`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: fillColor,
|
||||
transition: 'width 0.3s ease, background-color 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 文字(白色,居中,始终显示在最上层,带阴影效果) */}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center text-white text-2xl font-bold pointer-events-none"
|
||||
style={{
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.8)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MenuPuzzleCenterProps {
|
||||
/** 是否可见 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
/** 选择某个故居后的回调(进入对应的碎片系统) */
|
||||
onSelectResidence: (residence: string) => void
|
||||
/** 是否允许从管理中心进入具体拼图 */
|
||||
allowPuzzleEntry?: boolean
|
||||
}
|
||||
|
||||
const MenuPuzzleCenter = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSelectResidence,
|
||||
allowPuzzleEntry = true,
|
||||
}: MenuPuzzleCenterProps) => {
|
||||
const { getPuzzleCollectProgress } = usePuzzleStore()
|
||||
const [activeMapImage, setActiveMapImage] = useState<string | null>(null)
|
||||
|
||||
const residenceProgress = useMemo(
|
||||
() =>
|
||||
PUZZLE_RESIDENCES.map((item) => ({
|
||||
...item,
|
||||
// 只有已开放拼图的故居才计算收集进度
|
||||
progress: item.hasPuzzle ? getPuzzleCollectProgress(item.residence) : 0,
|
||||
})),
|
||||
[getPuzzleCollectProgress]
|
||||
)
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-90 flex items-center justify-center"
|
||||
style={{ background: 'rgba(0,0,0,0.8)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: getBackgroundImage('puzzle/home/底图') }}
|
||||
>
|
||||
{/* 箭头指示底图 */}
|
||||
<img
|
||||
className="absolute top-84 left-55"
|
||||
src={getStaticImage('puzzle/home/箭头指引')}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<Image
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage('components/关闭')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* 故居列表:按照设计图进行绝对定位 */}
|
||||
<div className="absolute inset-0">
|
||||
{residenceProgress.map(
|
||||
({
|
||||
residence,
|
||||
displayName,
|
||||
left,
|
||||
top,
|
||||
image,
|
||||
hasPuzzle,
|
||||
progress,
|
||||
hasMap,
|
||||
mapImage,
|
||||
}) => (
|
||||
<div
|
||||
key={residence}
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2`}
|
||||
style={{ left, top }}
|
||||
>
|
||||
{/* 故居图片(资源在 puzzle/home 下,可根据实际名称调整) */}
|
||||
<Image src={getStaticImage(`puzzle/home/${image}`)} alt={displayName} />
|
||||
|
||||
{/* 进度展示,仅对已开放拼图的故居显示 */}
|
||||
{hasPuzzle && (
|
||||
<div className="mb-4 flex justify-center">
|
||||
<HexagonProgressBar progress={progress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图标 */}
|
||||
<div className="flex justify-center gap-6">
|
||||
{allowPuzzleEntry && (
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/拼图`)}
|
||||
alt="拼图"
|
||||
button
|
||||
onClick={(e) => {
|
||||
if (!hasPuzzle) return
|
||||
e.stopPropagation()
|
||||
onSelectResidence(residence)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/追光入口`)}
|
||||
alt="导图"
|
||||
button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!hasMap || !mapImage) return
|
||||
setActiveMapImage(mapImage)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 导图弹窗 */}
|
||||
{activeMapImage && (
|
||||
<div
|
||||
className="fixed inset-0 z-110 flex items-center justify-center"
|
||||
style={{ background: 'rgba(0,0,0,0.75)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setActiveMapImage(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
className="absolute -right-4 -top-8 z-10"
|
||||
src={getStaticImage('components/close')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={() => setActiveMapImage(null)}
|
||||
/>
|
||||
|
||||
<img src={getStaticImage(`puzzle/home/map/${activeMapImage}`)} alt="导图" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuPuzzleCenter
|
||||
@@ -12,14 +12,14 @@ import { Image } from '@/components'
|
||||
*/
|
||||
const getResidenceDisplayName = (residence: string): string => {
|
||||
const nameMap: Record<string, string> = {
|
||||
chenyun: '陈云',
|
||||
huangyanpei: '黄炎培',
|
||||
luxun: '鲁迅',
|
||||
sunzhongshan: '孙中山',
|
||||
maozedong: '毛泽东',
|
||||
zhangleping: '张乐平',
|
||||
nieer: '聂耳',
|
||||
rongdesheng: '荣德生',
|
||||
chenyun: '陈云故居',
|
||||
huangyanpei: '黄炎培故居',
|
||||
luxun: '鲁迅故居',
|
||||
sunzhongshan: '孙中山故居',
|
||||
maozedong: '毛泽东旧居',
|
||||
zhangleping: '张乐平故居',
|
||||
nieer: '聂耳旧居',
|
||||
rongdesheng: '荣德生旧居',
|
||||
}
|
||||
|
||||
return nameMap[residence] || residence
|
||||
@@ -299,7 +299,7 @@ const PuzzleSystem = ({
|
||||
'[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]'
|
||||
)}
|
||||
>
|
||||
{getResidenceDisplayName(residence)}故居碎片
|
||||
{getResidenceDisplayName(residence)}碎片
|
||||
</span>
|
||||
<img
|
||||
className="absolute -top-10 left-0 -z-10"
|
||||
|
||||
@@ -86,7 +86,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
||||
}
|
||||
|
||||
setContentWidth(calculatedWidth)
|
||||
}, [fitContent, maxWidth, minWidth, options, placeholder])
|
||||
}, [fitContent, maxWidth, minWidth, options, placeholder, fontSize])
|
||||
|
||||
const updateDropdownPosition = useCallback(() => {
|
||||
if (!triggerRef.current) return
|
||||
@@ -240,7 +240,8 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-[36px] items-center justify-center border-b-2 border-dashed bg-transparent align-baseline outline-none',
|
||||
'mx-2 cursor-pointer whitespace-nowrap px-2 text-center text-[#4e402e]',
|
||||
'mx-2 cursor-pointer whitespace-nowrap px-2 text-center',
|
||||
selectedOption ? 'text-[#4e402e]' : 'text-[#999999]',
|
||||
error ? 'border-red-500' : 'border-[#4e402e]',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
className
|
||||
|
||||
Reference in new issue
Block a user