feat: 更新 ESLint 和 Prettier 配置,增强代码质量管理

- 在 .gitignore 中添加 public/audio/* 以忽略音频文件。
- 更新 bun.lock,添加 eslint-config-prettier 和 eslint-plugin-prettier 依赖,确保 ESLint 与 Prettier 的兼容性。
- 修改 eslint.config.ts,整合 Prettier 规则,优化代码风格一致性。
- 在 package.json 中添加 lint:fix 和 format 脚本,简化代码格式化和修复流程。
- 更新 tailwind.config.ts 和其他组件文件,统一代码风格,提升可读性。
This commit is contained in:
zhongyidian committed 2026-03-05 13:50:47 +08:00
1 parent 5d00adc1a5
commit 76c67b492e
216 files changed
+12454 -13976

No files matched your search

+3 -7
View File
@@ -1,7 +1,3 @@
export { default } from "./multiple-choice";
export { default as MultipleChoice } from "./multiple-choice";
export type {
MultipleChoiceProps,
MultipleChoiceOption,
} from "./multiple-choice.types";
export { default } from './multiple-choice'
export { default as MultipleChoice } from './multiple-choice'
export type { MultipleChoiceProps, MultipleChoiceOption } from './multiple-choice.types'
@@ -1,193 +1,188 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import MultipleChoice from "./multiple-choice";
import type { MultipleChoiceProps } from "./multiple-choice.types";
import { cn } from "@/lib/utils";
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import MultipleChoice from './multiple-choice'
import type { MultipleChoiceProps } from './multiple-choice.types'
import { cn } from '@/lib/utils'
const meta: Meta<typeof MultipleChoice> = {
title: "Components/Quiz/MultipleChoice",
title: 'Components/Quiz/MultipleChoice',
component: MultipleChoice,
tags: ["autodocs"],
tags: ['autodocs'],
argTypes: {
options: {
control: { type: "object" },
description: "选项列表,可以是字符串数组或选项对象数组",
control: { type: 'object' },
description: '选项列表,可以是字符串数组或选项对象数组',
},
value: {
control: { type: "object" },
description: "当前选中的值数组(受控模式)",
control: { type: 'object' },
description: '当前选中的值数组(受控模式)',
},
defaultValue: {
control: { type: "object" },
description: "默认选中的值数组",
control: { type: 'object' },
description: '默认选中的值数组',
},
disabled: {
control: { type: "boolean" },
description: "是否禁用整个组件",
control: { type: 'boolean' },
description: '是否禁用整个组件',
},
minSelections: {
control: { type: "number" },
description: "最小选择数量",
control: { type: 'number' },
description: '最小选择数量',
},
maxSelections: {
control: { type: "number" },
description: "最大选择数量",
control: { type: 'number' },
description: '最大选择数量',
},
showSubmitButton: {
control: { type: "boolean" },
description: "是否显示提交按钮",
control: { type: 'boolean' },
description: '是否显示提交按钮',
},
submitText: {
control: { type: "text" },
description: "提交按钮文本",
control: { type: 'text' },
description: '提交按钮文本',
},
},
};
}
export default meta;
type Story = StoryObj<typeof MultipleChoice>;
export default meta
type Story = StoryObj<typeof MultipleChoice>
// 基础示例
export const Default: Story = {
args: {
options: ["选项A", "选项B", "选项C", "选项D", "选项E"],
options: ['选项A', '选项B', '选项C', '选项D', '选项E'],
onSubmit: (values) => {
console.log("选中的值:", values);
console.log('选中的值:', values)
},
},
};
}
// 使用选项对象
export const WithOptionObjects: Story = {
args: {
options: [
{ value: "opt1", label: "选项一" },
{ value: "opt2", label: "选项二" },
{ value: "opt3", label: "选项三(禁用)", disabled: true },
{ value: "opt4", label: "选项四" },
{ value: "opt5", label: "选项五" },
{ value: 'opt1', label: '选项一' },
{ value: 'opt2', label: '选项二' },
{ value: 'opt3', label: '选项三(禁用)', disabled: true },
{ value: 'opt4', label: '选项四' },
{ value: 'opt5', label: '选项五' },
],
onSubmit: (values) => {
console.log("选中的值:", values);
console.log('选中的值:', values)
},
},
};
}
// 受控模式
export const Controlled: Story = {
render: () => {
const [values, setValues] = useState<string[]>([]);
const [values, setValues] = useState<string[]>([])
return (
<div className="space-y-4">
<MultipleChoice
options={["选项A", "选项B", "选项C", "选项D"]}
options={['选项A', '选项B', '选项C', '选项D']}
value={values}
onChange={setValues}
onSubmit={(vals) => {
alert(`提交的值: ${vals.join(", ")}`);
alert(`提交的值: ${vals.join(', ')}`)
}}
/>
<div className="text-sm text-gray-600">
当前选中: {values.length > 0 ? values.join(", ") : "未选择"}
当前选中: {values.length > 0 ? values.join(', ') : '未选择'}
</div>
</div>
);
)
},
};
}
// 限制最大选择数量
export const WithMaxSelections: Story = {
args: {
options: ["选项A", "选项B", "选项C", "选项D", "选项E"],
options: ['选项A', '选项B', '选项C', '选项D', '选项E'],
maxSelections: 2,
onSubmit: (values) => {
console.log("选中的值:", values);
console.log('选中的值:', values)
},
},
};
}
// 限制最小和最大选择数量
export const WithMinMaxSelections: Story = {
args: {
options: ["选项A", "选项B", "选项C", "选项D", "选项E"],
options: ['选项A', '选项B', '选项C', '选项D', '选项E'],
minSelections: 2,
maxSelections: 4,
onSubmit: (values) => {
console.log("选中的值:", values);
console.log('选中的值:', values)
},
},
};
}
// 禁用状态
export const Disabled: Story = {
args: {
options: ["选项A", "选项B", "选项C"],
options: ['选项A', '选项B', '选项C'],
disabled: true,
defaultValue: ["选项A"],
defaultValue: ['选项A'],
},
};
}
// 不显示提交按钮
export const WithoutSubmitButton: Story = {
args: {
options: ["选项A", "选项B", "选项C"],
options: ['选项A', '选项B', '选项C'],
showSubmitButton: false,
onChange: (values) => {
console.log("值变化:", values);
console.log('值变化:', values)
},
},
};
}
// 自定义样式
export const CustomStyled: Story = {
args: {
options: ["选项A", "选项B", "选项C", "选项D"],
className: "max-w-md mx-auto p-6 bg-gray-50 rounded-xl",
optionClassName: "text-lg font-semibold",
submitText: "确认答案",
options: ['选项A', '选项B', '选项C', '选项D'],
className: 'max-w-md mx-auto p-6 bg-gray-50 rounded-xl',
optionClassName: 'text-lg font-semibold',
submitText: '确认答案',
maxSelections: 3,
onSubmit: (values) => {
console.log("选中的值:", values);
console.log('选中的值:', values)
},
},
};
}
// 自定义渲染选项
export const CustomRender: Story = {
render: () => {
return (
<MultipleChoice
options={["选项A", "选项B", "选项C", "选项D"]}
options={['选项A', '选项B', '选项C', '选项D']}
maxSelections={2}
renderOption={(option, isSelected) => (
<div
className={cn(
"p-4 rounded-lg border-2 transition-all cursor-pointer",
isSelected
? "border-purple-500 bg-purple-100"
: "border-gray-200 bg-white"
'p-4 rounded-lg border-2 transition-all cursor-pointer',
isSelected ? 'border-purple-500 bg-purple-100' : 'border-gray-200 bg-white'
)}
>
<div className="flex items-center gap-3">
<div
className={cn(
"w-6 h-6 rounded border-2 flex items-center justify-center text-lg",
isSelected
? "border-purple-500 bg-purple-500 text-white"
: "border-gray-300"
'w-6 h-6 rounded border-2 flex items-center justify-center text-lg',
isSelected ? 'border-purple-500 bg-purple-500 text-white' : 'border-gray-300'
)}
>
{isSelected && "✓"}
{isSelected && '✓'}
</div>
<span className="font-medium">{option.label}</span>
</div>
</div>
)}
onSubmit={(values) => {
console.log("选中的值:", values);
console.log('选中的值:', values)
}}
/>
);
)
},
};
}
@@ -1,9 +1,6 @@
import { useState, useEffect, forwardRef } from "react";
import { cn } from "@/lib/utils";
import type {
MultipleChoiceProps,
MultipleChoiceOption,
} from "./multiple-choice.types";
import { useState, useEffect, forwardRef } from 'react'
import { cn } from '@/lib/utils'
import type { MultipleChoiceProps, MultipleChoiceOption } from './multiple-choice.types'
/**
* MultipleChoice - 多选组件
@@ -32,7 +29,7 @@ const MultipleChoice = forwardRef<HTMLDivElement, MultipleChoiceProps>(
maxSelections,
className,
optionClassName,
submitText = "提交",
submitText = '提交',
showSubmitButton = true,
renderOption,
},
@@ -40,96 +37,79 @@ const MultipleChoice = forwardRef<HTMLDivElement, MultipleChoiceProps>(
) => {
// 标准化选项数据
const normalizedOptions: MultipleChoiceOption[] = options.map((option) => {
if (typeof option === "string") {
return { value: option, label: option };
if (typeof option === 'string') {
return { value: option, label: option }
}
return option;
});
return option
})
// 受控/非受控模式
const isControlled = controlledValue !== undefined;
const [internalValue, setInternalValue] = useState<string[]>(
defaultValue || []
);
const currentValue = isControlled ? controlledValue : internalValue;
const isControlled = controlledValue !== undefined
const [internalValue, setInternalValue] = useState<string[]>(defaultValue || [])
const currentValue = isControlled ? controlledValue : internalValue
// 处理选项点击
const handleOptionClick = (optionValue: string) => {
if (disabled) return;
const option = normalizedOptions.find((opt) => opt.value === optionValue);
if (option?.disabled) return;
if (disabled) return
const option = normalizedOptions.find((opt) => opt.value === optionValue)
if (option?.disabled) return
const isSelected = currentValue.includes(optionValue);
let newValue: string[];
const isSelected = currentValue.includes(optionValue)
let newValue: string[]
if (isSelected) {
// 取消选择
newValue = currentValue.filter((v) => v !== optionValue);
newValue = currentValue.filter((v) => v !== optionValue)
// 检查最小选择数量限制
if (
minSelections !== undefined &&
newValue.length < minSelections
) {
return;
if (minSelections !== undefined && newValue.length < minSelections) {
return
}
} else {
// 选择
// 检查最大选择数量限制
if (
maxSelections !== undefined &&
currentValue.length >= maxSelections
) {
return;
if (maxSelections !== undefined && currentValue.length >= maxSelections) {
return
}
newValue = [...currentValue, optionValue];
newValue = [...currentValue, optionValue]
}
if (!isControlled) {
setInternalValue(newValue);
setInternalValue(newValue)
}
onChange?.(newValue);
};
onChange?.(newValue)
}
// 处理提交
const handleSubmit = () => {
if (disabled) return;
if (disabled) return
// 检查最小选择数量
if (
minSelections !== undefined &&
currentValue.length < minSelections
) {
return;
if (minSelections !== undefined && currentValue.length < minSelections) {
return
}
onSubmit?.(currentValue);
};
onSubmit?.(currentValue)
}
// 检查是否可以提交
const canSubmit =
currentValue.length > 0 &&
(minSelections === undefined ||
currentValue.length >= minSelections);
(minSelections === undefined || currentValue.length >= minSelections)
// 默认渲染选项
const defaultRenderOption = (
option: MultipleChoiceOption,
isSelected: boolean
) => {
const defaultRenderOption = (option: MultipleChoiceOption, isSelected: boolean) => {
const isDisabled =
disabled ||
option.disabled ||
(!isSelected &&
maxSelections !== undefined &&
currentValue.length >= maxSelections);
(!isSelected && maxSelections !== undefined && currentValue.length >= maxSelections)
return (
<div
className={cn(
"px-6 py-4 border-2 rounded-lg cursor-pointer transition-all",
"hover:border-green-400 hover:bg-green-50",
'px-6 py-4 border-2 rounded-lg cursor-pointer transition-all',
'hover:border-green-400 hover:bg-green-50',
isSelected
? "border-green-500 bg-green-100 text-green-700"
: "border-gray-300 bg-white text-gray-700",
isDisabled && "opacity-50 cursor-not-allowed",
? 'border-green-500 bg-green-100 text-green-700'
: 'border-gray-300 bg-white text-gray-700',
isDisabled && 'opacity-50 cursor-not-allowed',
optionClassName
)}
onClick={() => handleOptionClick(option.value)}
@@ -137,10 +117,8 @@ const MultipleChoice = forwardRef<HTMLDivElement, MultipleChoiceProps>(
<div className="flex items-center gap-3">
<div
className={cn(
"w-5 h-5 rounded border-2 flex items-center justify-center transition-colors",
isSelected
? "border-green-500 bg-green-500"
: "border-gray-300 bg-white"
'w-5 h-5 rounded border-2 flex items-center justify-center transition-colors',
isSelected ? 'border-green-500 bg-green-500' : 'border-gray-300 bg-white'
)}
>
{isSelected && (
@@ -162,21 +140,21 @@ const MultipleChoice = forwardRef<HTMLDivElement, MultipleChoiceProps>(
<span>{option.label}</span>
</div>
</div>
);
};
)
}
return (
<div ref={ref} className={cn("flex flex-col gap-4", className)}>
<div ref={ref} className={cn('flex flex-col gap-4', className)}>
<div className="flex flex-col gap-3">
{normalizedOptions.map((option) => {
const isSelected = currentValue.includes(option.value);
const isSelected = currentValue.includes(option.value)
return (
<div key={option.value}>
{renderOption
? renderOption(option, isSelected)
: defaultRenderOption(option, isSelected)}
</div>
);
)
})}
</div>
@@ -185,27 +163,22 @@ const MultipleChoice = forwardRef<HTMLDivElement, MultipleChoiceProps>(
{minSelections !== undefined && (
<div className="text-sm text-gray-500">
至少选择 {minSelections} 项
{maxSelections !== undefined &&
`,最多选择 ${maxSelections} 项`}
{maxSelections === undefined && "(无上限)"}
{maxSelections !== undefined && `,最多选择 ${maxSelections} 项`}
{maxSelections === undefined && '(无上限)'}
</div>
)}
{maxSelections !== undefined && minSelections === undefined && (
<div className="text-sm text-gray-500">
最多选择 {maxSelections} 项
</div>
<div className="text-sm text-gray-500">最多选择 {maxSelections} 项</div>
)}
<div className="text-sm text-gray-500">
已选择 {currentValue.length} 项
</div>
<div className="text-sm text-gray-500">已选择 {currentValue.length} 项</div>
<button
onClick={handleSubmit}
disabled={disabled || !canSubmit}
className={cn(
"px-6 py-3 rounded-lg font-medium transition-colors",
"bg-green-500 text-white hover:bg-green-600",
"disabled:opacity-50 disabled:cursor-not-allowed",
"focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
'px-6 py-3 rounded-lg font-medium transition-colors',
'bg-green-500 text-white hover:bg-green-600',
'disabled:opacity-50 disabled:cursor-not-allowed',
'focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2'
)}
>
{submitText}
@@ -213,11 +186,10 @@ const MultipleChoice = forwardRef<HTMLDivElement, MultipleChoiceProps>(
</div>
)}
</div>
);
)
}
);
)
MultipleChoice.displayName = "MultipleChoice";
export default MultipleChoice;
MultipleChoice.displayName = 'MultipleChoice'
export default MultipleChoice
@@ -1,45 +1,41 @@
import { ReactNode } from "react";
import { ReactNode } from 'react'
/** 选项数据 */
export interface MultipleChoiceOption {
/** 选项值 */
value: string;
value: string
/** 选项显示文本 */
label: string;
label: string
/** 是否禁用 */
disabled?: boolean;
disabled?: boolean
}
/** 多选组件配置项 */
export interface MultipleChoiceProps {
/** 选项列表 */
options: MultipleChoiceOption[] | string[];
options: MultipleChoiceOption[] | string[]
/** 当前选中的值数组(受控模式) */
value?: string[];
value?: string[]
/** 默认选中的值数组 */
defaultValue?: string[];
defaultValue?: string[]
/** 选中值变化时的回调 */
onChange?: (values: string[]) => void;
onChange?: (values: string[]) => void
/** 提交时的回调 */
onSubmit?: (values: string[]) => void;
onSubmit?: (values: string[]) => void
/** 是否禁用整个组件 */
disabled?: boolean;
disabled?: boolean
/** 最小选择数量 */
minSelections?: number;
minSelections?: number
/** 最大选择数量 */
maxSelections?: number;
maxSelections?: number
/** 自定义类名 */
className?: string;
className?: string
/** 选项容器的自定义类名 */
optionClassName?: string;
optionClassName?: string
/** 提交按钮文本 */
submitText?: string;
submitText?: string
/** 是否显示提交按钮 */
showSubmitButton?: boolean;
showSubmitButton?: boolean
/** 自定义渲染选项 */
renderOption?: (
option: MultipleChoiceOption,
isSelected: boolean
) => ReactNode;
renderOption?: (option: MultipleChoiceOption, isSelected: boolean) => ReactNode
}